Skip to content

Commit e94edf6

Browse files
committed
apm-api-shared
1 parent 2153aca commit e94edf6

219 files changed

Lines changed: 6739 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@
238238
"@kbn/anonymization-common": "link:x-pack/platform/packages/shared/ai-infra/anonymization-common",
239239
"@kbn/anonymization-plugin": "link:x-pack/platform/plugins/shared/anonymization",
240240
"@kbn/anonymization-ui": "link:x-pack/platform/packages/shared/ai-infra/anonymization-ui",
241+
"@kbn/apm-api-shared": "link:src/platform/packages/shared/kbn-apm-api-shared",
241242
"@kbn/apm-common": "link:src/platform/packages/shared/kbn-apm-common",
242243
"@kbn/apm-config-loader": "link:src/platform/packages/private/kbn-apm-config-loader",
243244
"@kbn/apm-data-access-plugin": "link:x-pack/solutions/observability/plugins/apm_data_access",
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
export {
11+
createCallApmApiV2,
12+
type APMClientV2,
13+
type AutoAbortedAPMClientV2,
14+
type APIReturnType,
15+
} from './src/create_call_apm_api';
16+
export { routeDefinitions } from './src/routes';
17+
export type { SharedAPMRouteRepository } from './src/routes';
18+
export type * from './src/routes/historical_data';
19+
export type * from './src/routes/suggestions';
20+
export type * from './src/routes/agent_keys';
21+
export type * from './src/routes/traces';
22+
export type * from './src/routes/span_links';
23+
export type * from './src/routes/observability_overview';
24+
export type * from './src/routes/agent_explorer';
25+
export type * from './src/routes/alerts';
26+
export type * from './src/routes/assistant_functions';
27+
export type * from './src/routes/correlations';
28+
export type * from './src/routes/custom_dashboards';
29+
export type * from './src/routes/dependencies';
30+
export type * from './src/routes/transactions';
31+
export type * from './src/routes/services';
32+
export type * from './src/routes/service_map';
33+
export type * from './src/routes/errors';
34+
export type * from './src/routes/infrastructure';
35+
export type * from './src/routes/environments';
36+
export type * from './src/routes/event_metadata';
37+
export type * from './src/routes/fallback_to_transactions';
38+
export type * from './src/routes/latency_distribution';
39+
export type * from './src/routes/metrics';
40+
export type * from './src/routes/profiling';
41+
export type * from './src/routes/service_groups';
42+
export type * from './src/routes/time_range_metadata';
43+
export type * from './src/routes/custom_links';
44+
export type * from './src/routes/anomaly_detection';
45+
export type * from './src/routes/mobile';
46+
export type * from './src/routes/mobile_errors';
47+
export type * from './src/routes/mobile_crashes';
48+
export type * from './src/routes/data_view';
49+
export type * from './src/routes/diagnostics';
50+
export type * from './src/routes/fleet';
51+
export type * from './src/routes/debug_telemetry';
52+
export type * from './src/routes/storage_explorer';
53+
export type * from './src/routes/source_maps';
54+
export type * from './src/routes/agent_configuration';
55+
export { sourceMapRt } from './src/routes/source_maps';
56+
export { filterOptionsRt, payloadRt } from './src/routes/custom_links';
57+
export {
58+
rangeRt,
59+
kueryRt,
60+
probabilityRt,
61+
offsetRt,
62+
serviceTransactionDataSourceRt,
63+
transactionDataSourceRt,
64+
filtersRt,
65+
} from './src/default_api_types';
66+
export {
67+
OBSERVABILITY_APM_CPS_ENABLED_DEFAULT,
68+
OBSERVABILITY_APM_CPS_ENABLED_FEATURE_FLAG,
69+
} from './src/cps_feature_flag';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"type": "shared-common",
3+
"id": "@kbn/apm-api-shared",
4+
"owner": [
5+
"@elastic/obs-presentation-team",
6+
"@elastic/obs-exploration-team"
7+
],
8+
"group": "platform",
9+
"visibility": "shared"
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "@kbn/apm-api-shared",
3+
"private": true,
4+
"version": "1.0.0",
5+
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
6+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { CoreSetup, CoreStart } from '@kbn/core/public';
11+
import { isString, startsWith } from 'lodash';
12+
import { LRUCache as LRU } from 'lru-cache';
13+
import hash from 'object-hash';
14+
import type { FetchOptions } from './create_call_apm_api';
15+
16+
function fetchOptionsWithDebug(fetchOptions: FetchOptions, inspectableEsQueriesEnabled: boolean) {
17+
const debugEnabled =
18+
inspectableEsQueriesEnabled && startsWith(fetchOptions.pathname, '/internal/apm');
19+
20+
const { body, ...rest } = fetchOptions;
21+
22+
return {
23+
...rest,
24+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
25+
query: {
26+
...fetchOptions.query,
27+
...(debugEnabled ? { _inspect: true } : {}),
28+
},
29+
};
30+
}
31+
32+
const cache = new LRU<string, any>({ max: 100, ttl: 1000 * 60 * 60 });
33+
34+
export function clearCache() {
35+
cache.clear();
36+
}
37+
38+
export type CallApi = typeof callApi;
39+
40+
export async function callApi<T = void>(
41+
{ http, uiSettings }: CoreStart | CoreSetup,
42+
fetchOptions: FetchOptions
43+
): Promise<T> {
44+
const inspectableEsQueriesEnabled: boolean =
45+
// For now this needs to be hardcoded as we cannot import the key, as it lives inside the observability plugin,
46+
// and refactoring it is outside the scope of this PR, but ideally this should be imported from the same place as the key is defined
47+
uiSettings?.get('observability:enableInspectEsQueries') ?? false;
48+
const cacheKey = getCacheKey(fetchOptions);
49+
const cacheResponse = cache.get(cacheKey);
50+
if (cacheResponse) {
51+
return cacheResponse;
52+
}
53+
54+
const {
55+
pathname,
56+
method = 'get',
57+
...options
58+
} = fetchOptionsWithDebug(fetchOptions, inspectableEsQueriesEnabled);
59+
60+
const lowercaseMethod = method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch';
61+
62+
const res = await http[lowercaseMethod]<T>(pathname, options);
63+
64+
if (isCachable(fetchOptions)) {
65+
cache.set(cacheKey, res);
66+
}
67+
68+
return res;
69+
}
70+
71+
// only cache items that has a time range with `start` and `end` params,
72+
// and where `end` is not a timestamp in the future
73+
function isCachable(fetchOptions: FetchOptions) {
74+
if (fetchOptions.isCachable !== undefined) {
75+
return fetchOptions.isCachable;
76+
}
77+
78+
if (!(fetchOptions.query && fetchOptions.query.start && fetchOptions.query.end)) {
79+
return false;
80+
}
81+
82+
return (
83+
isString(fetchOptions.query.end) && new Date(fetchOptions.query.end).getTime() < Date.now()
84+
);
85+
}
86+
87+
// order the options object to make sure that two objects with the same arguments, produce produce the
88+
// same cache key regardless of the order of properties
89+
function getCacheKey(options: FetchOptions) {
90+
const { pathname, method, body, query, headers } = options;
91+
return hash({ pathname, method, body, query, headers });
92+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
/** Use with `feature_flags.overrides` in kibana.yml to toggle CPS integration for APM. */
11+
export const OBSERVABILITY_APM_CPS_ENABLED_FEATURE_FLAG = 'observability.apm.cpsEnabled' as const;
12+
13+
/**
14+
* Fallback when the flag is unset and no override exists (same default as the removed
15+
* `xpack.apm.featureFlags.apmCPSEnabled` setting).
16+
*/
17+
export const OBSERVABILITY_APM_CPS_ENABLED_DEFAULT = true;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
import type { CoreStart } from '@kbn/core/public';
10+
import type { EndpointOf, ReturnOf } from '@kbn/server-route-repository-utils';
11+
import { formatRequest } from '@kbn/server-route-repository-utils';
12+
import { type RouteRepositoryClient } from '@kbn/server-route-repository';
13+
import type { HttpFetchOptions } from '@kbn/core/public';
14+
import type { ICPSManager } from '@kbn/cps-utils';
15+
import type { SharedAPMRouteRepository } from './routes';
16+
import type { CallApi } from './call_api';
17+
import { callApi } from './call_api';
18+
import {
19+
OBSERVABILITY_APM_CPS_ENABLED_DEFAULT,
20+
OBSERVABILITY_APM_CPS_ENABLED_FEATURE_FLAG,
21+
} from './cps_feature_flag';
22+
23+
export type FetchOptions = Omit<HttpFetchOptions, 'body'> & {
24+
pathname: string;
25+
isCachable?: boolean;
26+
method?: string;
27+
body?: any;
28+
};
29+
30+
type APMClientOptions = Omit<FetchOptions, 'query' | 'body' | 'pathname' | 'signal'> & {
31+
signal: AbortSignal | null;
32+
};
33+
34+
export type APMClientV2 = RouteRepositoryClient<
35+
SharedAPMRouteRepository,
36+
APMClientOptions
37+
>['fetch'];
38+
39+
export type AutoAbortedAPMClientV2 = RouteRepositoryClient<
40+
SharedAPMRouteRepository,
41+
Omit<APMClientOptions, 'signal'>
42+
>['fetch'];
43+
44+
type APIEndpoint = EndpointOf<SharedAPMRouteRepository>;
45+
46+
export type APIReturnType<TEndpoint extends APIEndpoint> = ReturnOf<
47+
SharedAPMRouteRepository,
48+
TEndpoint
49+
>;
50+
51+
interface Dependencies {
52+
cpsManager?: ICPSManager;
53+
}
54+
55+
export function createCallApmApiV2(
56+
core: CoreStart,
57+
{ cpsManager }: Dependencies = {}
58+
): APMClientV2 {
59+
return ((endpoint, options) => {
60+
const { params } = options as unknown as {
61+
params?: Partial<Record<string, any>>;
62+
};
63+
64+
const { method, pathname, version } = formatRequest(endpoint, params?.path);
65+
const isCpsEnabled = core.featureFlags.getBooleanValue(
66+
OBSERVABILITY_APM_CPS_ENABLED_FEATURE_FLAG,
67+
OBSERVABILITY_APM_CPS_ENABLED_DEFAULT
68+
);
69+
const projectRouting = isCpsEnabled ? cpsManager?.getProjectRouting() : undefined;
70+
71+
return callApi(core, {
72+
...options,
73+
method,
74+
pathname,
75+
body: params?.body,
76+
query: params?.query,
77+
version,
78+
headers: {
79+
...(options as any)?.headers,
80+
...(projectRouting ? { 'x-project-routing': projectRouting } : {}),
81+
},
82+
} as unknown as Parameters<CallApi>[1]);
83+
}) as APMClientV2;
84+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
import * as t from 'io-ts';
10+
import { either } from 'fp-ts/Either';
11+
import { isoToEpochRt, toNumberRt } from '@kbn/io-ts-utils';
12+
import type { BoolQuery } from '@kbn/es-query';
13+
import { ApmDocumentType, RollupInterval } from '@kbn/apm-types';
14+
15+
export const rangeRt = t.type({
16+
start: isoToEpochRt,
17+
end: isoToEpochRt,
18+
});
19+
20+
export const kueryRt = t.type({ kuery: t.string });
21+
22+
export const probabilityRt = t.type({
23+
probability: toNumberRt,
24+
});
25+
26+
export const offsetRt = t.partial({
27+
offset: t.string,
28+
});
29+
30+
export const serviceTransactionDataSourceRt = t.type({
31+
documentType: t.union([
32+
t.literal(ApmDocumentType.ServiceTransactionMetric),
33+
t.literal(ApmDocumentType.TransactionMetric),
34+
t.literal(ApmDocumentType.TransactionEvent),
35+
]),
36+
rollupInterval: t.union([
37+
t.literal(RollupInterval.OneMinute),
38+
t.literal(RollupInterval.TenMinutes),
39+
t.literal(RollupInterval.SixtyMinutes),
40+
t.literal(RollupInterval.None),
41+
]),
42+
});
43+
44+
export const transactionDataSourceRt = t.type({
45+
documentType: t.union([
46+
t.literal(ApmDocumentType.TransactionMetric),
47+
t.literal(ApmDocumentType.TransactionEvent),
48+
]),
49+
rollupInterval: t.union([
50+
t.literal(RollupInterval.OneMinute),
51+
t.literal(RollupInterval.TenMinutes),
52+
t.literal(RollupInterval.SixtyMinutes),
53+
t.literal(RollupInterval.None),
54+
]),
55+
});
56+
57+
const BoolQueryRt = t.type({
58+
should: t.array(t.record(t.string, t.unknown)),
59+
must: t.array(t.record(t.string, t.unknown)),
60+
must_not: t.array(t.record(t.string, t.unknown)),
61+
filter: t.array(t.record(t.string, t.unknown)),
62+
});
63+
64+
export const filtersRt = new t.Type<BoolQuery, string, unknown>(
65+
'BoolQuery',
66+
BoolQueryRt.is,
67+
(input: unknown, context: t.Context) =>
68+
either.chain(t.string.validate(input, context), (value: string) => {
69+
try {
70+
const filters = JSON.parse(value);
71+
const decoded = {
72+
should: [],
73+
must: [],
74+
must_not: filters.must_not ? [...filters.must_not] : [],
75+
filter: filters.filter ? [...filters.filter] : [],
76+
};
77+
return t.success(decoded);
78+
} catch (err) {
79+
return t.failure(input, context, err.message);
80+
}
81+
}),
82+
(filters: BoolQuery): string => JSON.stringify(filters)
83+
);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
import type { CoreStart } from '@kbn/core/public';
10+
import { createCallApmApiV2 } from './create_call_apm_api';
11+
12+
const coreMock = {
13+
featureFlags: { getBooleanValue: () => false },
14+
} as unknown as CoreStart;
15+
16+
export function mockCreateCallApmApiV2(core: CoreStart = {} as CoreStart) {
17+
return createCallApmApiV2({ ...coreMock, ...core });
18+
}

0 commit comments

Comments
 (0)