diff --git a/src/core/packages/feature-flags/server-internal/moon.yml b/src/core/packages/feature-flags/server-internal/moon.yml index aed6b3a278534..805050a183ebd 100644 --- a/src/core/packages/feature-flags/server-internal/moon.yml +++ b/src/core/packages/feature-flags/server-internal/moon.yml @@ -17,6 +17,7 @@ project: owner: '@elastic/kibana-core' sourceRoot: src/core/packages/feature-flags/server-internal dependsOn: + - '@kbn/apm-utils' - '@kbn/core-base-server-internal' - '@kbn/core-feature-flags-server' - '@kbn/logging' diff --git a/src/core/packages/feature-flags/server-internal/src/feature_flags_service.test.ts b/src/core/packages/feature-flags/server-internal/src/feature_flags_service.test.ts index f7b5ef756fe78..1b1dbf1b54f93 100644 --- a/src/core/packages/feature-flags/server-internal/src/feature_flags_service.test.ts +++ b/src/core/packages/feature-flags/server-internal/src/feature_flags_service.test.ts @@ -167,19 +167,19 @@ describe('FeatureFlagsService Server', () => { test('get boolean flag', async () => { const value = false; await expect(startContract.getBooleanValue('my-flag', value)).resolves.toEqual(value); - expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }); + expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }, undefined); }); test('get string flag', async () => { const value = 'my-default'; await expect(startContract.getStringValue('my-flag', value)).resolves.toEqual(value); - expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }); + expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }, undefined); }); test('get number flag', async () => { const value = 42; await expect(startContract.getNumberValue('my-flag', value)).resolves.toEqual(value); - expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }); + expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }, undefined); }); test('observe a boolean flag', async () => { @@ -189,7 +189,7 @@ describe('FeatureFlagsService Server', () => { flag$.subscribe((v) => observedValues.push(v)); // Initial emission await expect(firstValueFrom(flag$)).resolves.toEqual(value); - expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }); + expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }, undefined); expect(observedValues).toHaveLength(1); // Does not reevaluate and emit if the other flags are changed @@ -215,7 +215,7 @@ describe('FeatureFlagsService Server', () => { flag$.subscribe((v) => observedValues.push(v)); // Initial emission await expect(firstValueFrom(flag$)).resolves.toEqual(value); - expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }); + expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }, undefined); expect(observedValues).toHaveLength(1); // Does not reevaluate and emit if the other flags are changed @@ -241,7 +241,7 @@ describe('FeatureFlagsService Server', () => { flag$.subscribe((v) => observedValues.push(v)); // Initial emission await expect(firstValueFrom(flag$)).resolves.toEqual(value); - expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }); + expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value }, undefined); expect(observedValues).toHaveLength(1); // Does not reevaluate and emit if the other flags are changed @@ -265,7 +265,7 @@ describe('FeatureFlagsService Server', () => { await expect(startContract.getBooleanValue('my-overridden-flag', false)).resolves.toEqual( true ); - expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-overridden-flag': true }); + expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-overridden-flag': true }, undefined); expect(getBooleanValueSpy).not.toHaveBeenCalled(); // Only to prove the spy works @@ -280,7 +280,7 @@ describe('FeatureFlagsService Server', () => { flag$.subscribe((v) => observedValues.push(v)); // Initial emission await expect(firstValueFrom(flag$)).resolves.toEqual(true); - expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-overridden-flag': true }); + expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-overridden-flag': true }, undefined); expect(observedValues).toHaveLength(1); // Does not reevaluate and emit if the other flags are changed diff --git a/src/core/packages/feature-flags/server-internal/src/feature_flags_service.ts b/src/core/packages/feature-flags/server-internal/src/feature_flags_service.ts index ce8b884d9132d..97a733525708d 100644 --- a/src/core/packages/feature-flags/server-internal/src/feature_flags_service.ts +++ b/src/core/packages/feature-flags/server-internal/src/feature_flags_service.ts @@ -15,7 +15,7 @@ import type { MultiContextEvaluationContext, } from '@kbn/core-feature-flags-server'; import type { Logger } from '@kbn/logging'; -import apm from 'elastic-apm-node'; +import { addSpanLabels } from '@kbn/apm-utils'; import { getFlattenedObject } from '@kbn/std'; import { type Client, @@ -215,7 +215,7 @@ export class FeatureFlagsService { ? (override as T) : // We have to bind the evaluation or the client will lose its internal context await evaluationFn.bind(this.featureFlagsClient)(flagName, fallbackValue); - apm.addLabels({ [`flag_${flagName.replaceAll('.', '_')}`]: value }); + addSpanLabels({ [`flag_${flagName.replaceAll('.', '_')}`]: value }); // TODO: increment usage counter return value; } diff --git a/src/core/packages/feature-flags/server-internal/tsconfig.json b/src/core/packages/feature-flags/server-internal/tsconfig.json index 38dfc18573de1..b88b980319552 100644 --- a/src/core/packages/feature-flags/server-internal/tsconfig.json +++ b/src/core/packages/feature-flags/server-internal/tsconfig.json @@ -14,6 +14,7 @@ "target/**/*" ], "kbn_references": [ + "@kbn/apm-utils", "@kbn/core-base-server-internal", "@kbn/core-feature-flags-server", "@kbn/logging", diff --git a/src/core/packages/http/server-internal/moon.yml b/src/core/packages/http/server-internal/moon.yml index c06ae0238d5f6..982ce26e4a53b 100644 --- a/src/core/packages/http/server-internal/moon.yml +++ b/src/core/packages/http/server-internal/moon.yml @@ -47,6 +47,7 @@ dependsOn: - '@kbn/core-user-activity-server-mocks' - '@kbn/core-security-common' - '@kbn/spaces-utils' + - '@kbn/apm-utils' tags: - shared-server - package diff --git a/src/core/packages/http/server-internal/src/http_server.ts b/src/core/packages/http/server-internal/src/http_server.ts index c83f37aa4fabf..107a8b8f4a7c8 100644 --- a/src/core/packages/http/server-internal/src/http_server.ts +++ b/src/core/packages/http/server-internal/src/http_server.ts @@ -17,6 +17,7 @@ import type { Duration } from 'moment'; import type { Observable, Subscription } from 'rxjs'; import { firstValueFrom, pairwise, take } from 'rxjs'; import apm from 'elastic-apm-node'; +import { addSpanLabels, addTransactionLabels } from '@kbn/apm-utils'; import Brok from 'brok'; import type { Logger, LoggerFactory } from '@kbn/logging'; import type { AuthenticatedUser } from '@kbn/core-security-common'; @@ -76,7 +77,7 @@ function startEluMeasurement( path: string, log: Logger, eluMonitorOptions: IHttpEluMonitorConfig | undefined -): (httpSpan?: OTelSpan) => void { +): () => void { if (!eluMonitorOptions?.enabled) { return identity; } @@ -84,20 +85,22 @@ function startEluMeasurement( const startUtilization = performance.eventLoopUtilization(); const start = performance.now(); - return function stopEluMeasurement(httpSpan?: OTelSpan) { + return function stopEluMeasurement() { const { active, utilization } = performance.eventLoopUtilization(startUtilization); - apm.currentTransaction?.addLabels( + addTransactionLabels( { event_loop_utilization: utilization, event_loop_active: active, }, - false + { + isString: false, + otelAttributes: { + 'nodejs.eventloop.utilization': utilization, + 'nodejs.eventloop.active': active, + }, + } ); - httpSpan?.setAttributes({ - 'nodejs.eventloop.utilization': utilization, - 'nodejs.eventloop.active': active, - }); const duration = performance.now() - start; @@ -601,11 +604,11 @@ export class HttpServer { } if (isBoom(request.response)) { - stop(app.httpSpan); + stop(); app.otelSubSpan?.end(); } else { request.response.events.once('finish', () => { - stop(app.httpSpan); + stop(); app.otelSubSpan?.end(); }); } @@ -635,18 +638,17 @@ export class HttpServer { if (executionContext && parentContext) { executionContext.set(parentContext); const labels = executionContext.getAsLabels(); - apm.addLabels(labels); const { name, id, page } = labels; - trace.getActiveSpan()?.setAttributes( - omitBy( + addSpanLabels(labels, { + otelAttributes: omitBy( { 'kibana.execution_context.name': name, 'kibana.execution_context.id': id, 'kibana.execution_context.page': page, }, isNil - ) as Record - ); + ) as Record, + }); } executionContext?.setRequestId(requestId); diff --git a/src/core/packages/http/server-internal/tsconfig.json b/src/core/packages/http/server-internal/tsconfig.json index 562fc2e61b97c..b1124dda187d2 100644 --- a/src/core/packages/http/server-internal/tsconfig.json +++ b/src/core/packages/http/server-internal/tsconfig.json @@ -41,6 +41,7 @@ "@kbn/core-user-activity-server-mocks", "@kbn/core-security-common", "@kbn/spaces-utils", + "@kbn/apm-utils", ], "exclude": [ "target/**/*", diff --git a/src/platform/packages/shared/kbn-apm-utils/index.ts b/src/platform/packages/shared/kbn-apm-utils/index.ts index 43b9ebcf13620..dfb618a239982 100644 --- a/src/platform/packages/shared/kbn-apm-utils/index.ts +++ b/src/platform/packages/shared/kbn-apm-utils/index.ts @@ -8,3 +8,4 @@ */ export { withSpan, type SpanOptions, parseSpanOptions } from './src/with_span'; export { instrumentAsyncMethods } from './src/instrument_async_methods'; +export { addSpanLabels, addTransactionLabels, type Labels } from './src/add_labels'; diff --git a/src/platform/packages/shared/kbn-apm-utils/src/add_labels.test.ts b/src/platform/packages/shared/kbn-apm-utils/src/add_labels.test.ts new file mode 100644 index 0000000000000..11866e7dfb479 --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-utils/src/add_labels.test.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { trace } from '@opentelemetry/api'; +import type { AttributeValue } from '@opentelemetry/api'; +import apm from 'elastic-apm-node'; + +import { addSpanLabels, addTransactionLabels } from './add_labels'; +import type { Labels } from './add_labels'; + +jest.mock('elastic-apm-node', () => ({ + __esModule: true, + default: { + addLabels: jest.fn(), + currentTransaction: { + addLabels: jest.fn(), + }, + }, +})); + +jest.mock('@opentelemetry/api', () => ({ + trace: { + getActiveSpan: jest.fn(), + }, +})); + +interface MockSpan { + setAttributes: jest.Mock]>; +} + +interface MockApm { + addLabels: jest.Mock; + currentTransaction?: { + addLabels: jest.Mock; + }; +} + +const mockedApm = apm as unknown as MockApm; +const getActiveSpanMock = trace.getActiveSpan as jest.MockedFunction; + +const createMockSpan = (): MockSpan => ({ + setAttributes: jest.fn(), +}); + +const getTransactionAddLabelsMock = (): jest.Mock => { + if (!mockedApm.currentTransaction) { + throw new Error('expected currentTransaction mock to be defined'); + } + + return mockedApm.currentTransaction.addLabels; +}; + +describe('add_labels', () => { + beforeEach(() => { + mockedApm.addLabels.mockClear(); + getTransactionAddLabelsMock().mockClear(); + getActiveSpanMock.mockReset(); + }); + + describe('addSpanLabels', () => { + it('calls apm.addLabels with original labels', () => { + const labels: Labels = { + foo: 'bar', + count: 3, + }; + const span = createMockSpan(); + + getActiveSpanMock.mockReturnValue(span as never); + + addSpanLabels(labels); + + expect(mockedApm.addLabels).toHaveBeenCalledWith(labels, undefined); + }); + + it('calls setAttributes on active OTel span with kibana.-prefixed keys', () => { + const span = createMockSpan(); + + getActiveSpanMock.mockReturnValue(span as never); + + addSpanLabels({ + foo: 'bar', + count: 3, + enabled: true, + }); + + expect(span.setAttributes).toHaveBeenCalledWith({ + 'kibana.foo': 'bar', + 'kibana.count': 3, + 'kibana.enabled': true, + }); + }); + + it('filters out null and undefined label values before OTel write', () => { + const span = createMockSpan(); + + getActiveSpanMock.mockReturnValue(span as never); + + addSpanLabels({ + kept: 'value', + nullish: null, + missing: undefined, + zero: 0, + disabled: false, + }); + + expect(span.setAttributes).toHaveBeenCalledWith({ + 'kibana.kept': 'value', + 'kibana.zero': 0, + 'kibana.disabled': false, + }); + }); + + it('with otelAttributes uses provided attributes instead of auto-prefixed', () => { + const span = createMockSpan(); + const otelAttributes = { + 'custom.foo': 'bar', + 'custom.count': 42, + }; + + getActiveSpanMock.mockReturnValue(span as never); + + addSpanLabels( + { + foo: 'ignored', + }, + { otelAttributes } + ); + + expect(span.setAttributes).toHaveBeenCalledWith(otelAttributes); + }); + + it('forwards isString option to apm.addLabels', () => { + const span = createMockSpan(); + const labels = { + foo: 123, + }; + + getActiveSpanMock.mockReturnValue(span as never); + + addSpanLabels(labels, { isString: true }); + + expect(mockedApm.addLabels).toHaveBeenCalledWith(labels, true); + }); + }); + + describe('addTransactionLabels', () => { + it('calls apm.currentTransaction.addLabels with original labels', () => { + const labels: Labels = { + foo: 'bar', + count: 3, + }; + const span = createMockSpan(); + + getActiveSpanMock.mockReturnValue(span as never); + + addTransactionLabels(labels); + + expect(getTransactionAddLabelsMock()).toHaveBeenCalledWith(labels, undefined); + }); + + it('calls setAttributes on active OTel span with kibana.-prefixed keys', () => { + const span = createMockSpan(); + + getActiveSpanMock.mockReturnValue(span as never); + + addTransactionLabels({ + foo: 'bar', + count: 3, + enabled: true, + }); + + expect(span.setAttributes).toHaveBeenCalledWith({ + 'kibana.foo': 'bar', + 'kibana.count': 3, + 'kibana.enabled': true, + }); + }); + + it('forwards isString option to apm.currentTransaction.addLabels', () => { + const span = createMockSpan(); + const labels = { + foo: 123, + }; + + getActiveSpanMock.mockReturnValue(span as never); + + addTransactionLabels(labels, { isString: true }); + + expect(getTransactionAddLabelsMock()).toHaveBeenCalledWith(labels, true); + }); + }); + + it('no-ops on the OTel side when trace.getActiveSpan() returns undefined', () => { + const labels = { + foo: 'bar', + }; + + getActiveSpanMock.mockReturnValue(undefined); + + expect(() => { + addSpanLabels(labels); + addTransactionLabels(labels); + }).not.toThrow(); + + expect(mockedApm.addLabels).toHaveBeenCalledWith(labels, undefined); + expect(getTransactionAddLabelsMock()).toHaveBeenCalledWith(labels, undefined); + }); + + it('handles an empty labels object gracefully', () => { + const span = createMockSpan(); + + getActiveSpanMock.mockReturnValue(span as never); + + expect(() => addSpanLabels({})).not.toThrow(); + expect(() => addTransactionLabels({})).not.toThrow(); + + expect(span.setAttributes).toHaveBeenNthCalledWith(1, {}); + expect(span.setAttributes).toHaveBeenNthCalledWith(2, {}); + }); +}); diff --git a/src/platform/packages/shared/kbn-apm-utils/src/add_labels.ts b/src/platform/packages/shared/kbn-apm-utils/src/add_labels.ts new file mode 100644 index 0000000000000..c565a00f175e7 --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-utils/src/add_labels.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { trace } from '@opentelemetry/api'; +import type { AttributeValue } from '@opentelemetry/api'; +import apm from 'elastic-apm-node'; + +export type Labels = Record; + +interface AddLabelsOptions { + otelAttributes?: Record; + isString?: boolean; +} + +export const prefixKeys = (labels: Labels, prefix: string): Record => + Object.fromEntries( + Object.entries(labels) + .filter(([, v]) => v != null) + .map(([k, v]) => [`${prefix}${k}`, v as AttributeValue]) + ); + +export const addSpanLabels = (labels: Labels, opts?: AddLabelsOptions): void => { + apm.addLabels(labels, opts?.isString); + trace.getActiveSpan()?.setAttributes(opts?.otelAttributes ?? prefixKeys(labels, 'kibana.')); +}; + +export const addTransactionLabels = (labels: Labels, opts?: AddLabelsOptions): void => { + apm.currentTransaction?.addLabels(labels, opts?.isString); + trace.getActiveSpan()?.setAttributes(opts?.otelAttributes ?? prefixKeys(labels, 'kibana.')); +}; diff --git a/src/platform/packages/shared/kbn-apm-utils/src/with_span.ts b/src/platform/packages/shared/kbn-apm-utils/src/with_span.ts index 9355b7e552919..2553fbe4c2462 100644 --- a/src/platform/packages/shared/kbn-apm-utils/src/with_span.ts +++ b/src/platform/packages/shared/kbn-apm-utils/src/with_span.ts @@ -9,8 +9,10 @@ import type { Logger } from 'elastic-apm-node'; import agent from 'elastic-apm-node'; +import { trace } from '@opentelemetry/api'; import { withActiveSpan } from '@kbn/tracing-utils'; import asyncHooks from 'async_hooks'; +import { prefixKeys } from './add_labels'; export interface SpanOptions { name: string; @@ -72,7 +74,16 @@ export async function withSpan( if (!agent.isStarted()) { // If Elastic APM is not started, let's use OTel's withActiveSpan instead in case it's started. - const promise = withActiveSpan(name, { attributes: { 'span.type': type } }, () => cb()); + const promise = withActiveSpan( + name, + { + attributes: { + 'span.type': type, + ...(labels ? prefixKeys(labels, 'kibana.') : {}), + }, + }, + () => cb() + ); // make sure tests that mock out the callback with a sync // function don't fail. if (typeof promise === 'object' && 'then' in promise) { @@ -132,6 +143,7 @@ export async function withSpan( if (labels) { targetedSpan.addLabels(labels); + trace.getActiveSpan()?.setAttributes(prefixKeys(labels, 'kibana.')); } return promise diff --git a/src/platform/plugins/shared/data/moon.yml b/src/platform/plugins/shared/data/moon.yml index 29035e37f6d51..b7087e6237ae8 100644 --- a/src/platform/plugins/shared/data/moon.yml +++ b/src/platform/plugins/shared/data/moon.yml @@ -67,6 +67,7 @@ dependsOn: - '@kbn/core-application-browser-mocks' - '@kbn/cps' - '@kbn/cps-utils' + - '@kbn/apm-utils' - '@kbn/std' - '@kbn/core-elasticsearch-server' tags: diff --git a/src/platform/plugins/shared/data/server/search/routes/search.ts b/src/platform/plugins/shared/data/server/search/routes/search.ts index 892cfa6112edb..3e268c6bca722 100644 --- a/src/platform/plugins/shared/data/server/search/routes/search.ts +++ b/src/platform/plugins/shared/data/server/search/routes/search.ts @@ -8,13 +8,13 @@ */ import { first } from 'rxjs'; +import { addSpanLabels } from '@kbn/apm-utils'; import { schema } from '@kbn/config-schema'; import { reportServerError } from '@kbn/kibana-utils-plugin/server'; import type { IncomingMessage } from 'http'; import type { KibanaExecutionContext } from '@kbn/core-execution-context-common'; import type { Logger } from '@kbn/logging'; import type { ExecutionContextSetup } from '@kbn/core-execution-context-server'; -import apm from 'elastic-apm-node'; import { reportSearchError } from '../report_search_error'; import { getRequestAbortedSignal } from '../../lib'; import type { DataPluginRouter } from '../types'; @@ -90,7 +90,7 @@ export function registerSearchRoute( } return executionContextSetup.withContext(executionContext, async () => { - apm.addLabels(executionContextSetup.getAsLabels()); + addSpanLabels(executionContextSetup.getAsLabels()); try { const search = await context.search; const response = await search diff --git a/src/platform/plugins/shared/data/tsconfig.json b/src/platform/plugins/shared/data/tsconfig.json index 2f66133042fbc..9ec9e8bf4bada 100644 --- a/src/platform/plugins/shared/data/tsconfig.json +++ b/src/platform/plugins/shared/data/tsconfig.json @@ -63,6 +63,7 @@ "@kbn/core-application-browser-mocks", "@kbn/cps", "@kbn/cps-utils", + "@kbn/apm-utils", "@kbn/std", "@kbn/core-elasticsearch-server", ], diff --git a/src/platform/plugins/shared/telemetry_collection_manager/moon.yml b/src/platform/plugins/shared/telemetry_collection_manager/moon.yml index a04c1eb5c0486..dfb49a8954195 100644 --- a/src/platform/plugins/shared/telemetry_collection_manager/moon.yml +++ b/src/platform/plugins/shared/telemetry_collection_manager/moon.yml @@ -20,6 +20,7 @@ dependsOn: - '@kbn/core' - '@kbn/usage-collection-plugin' - '@kbn/tracing-utils' + - '@kbn/apm-utils' tags: - plugin - prod diff --git a/src/platform/plugins/shared/telemetry_collection_manager/server/plugin.ts b/src/platform/plugins/shared/telemetry_collection_manager/server/plugin.ts index 4acacefacc33f..77aa6343df8d7 100644 --- a/src/platform/plugins/shared/telemetry_collection_manager/server/plugin.ts +++ b/src/platform/plugins/shared/telemetry_collection_manager/server/plugin.ts @@ -22,6 +22,7 @@ import type { } from '@kbn/core/server'; import { firstValueFrom, ReplaySubject } from 'rxjs'; +import { addSpanLabels } from '@kbn/apm-utils'; import apm from 'elastic-apm-node'; import { SpanStatusCode, type SpanOptions } from '@opentelemetry/api'; import { withActiveSpan } from '@kbn/tracing-utils'; @@ -278,7 +279,7 @@ export class TelemetryCollectionManagerPlugin 'Retrieve Snapshot Telemetry', 'telemetry' ); - retrieveSnapshotTelemetryTransaction.addLabels({ + addSpanLabels({ unencrypted: config.unencrypted, refresh: config.refreshCache, }); diff --git a/src/platform/plugins/shared/telemetry_collection_manager/tsconfig.json b/src/platform/plugins/shared/telemetry_collection_manager/tsconfig.json index aed1afb731e3c..c2c98504cd925 100644 --- a/src/platform/plugins/shared/telemetry_collection_manager/tsconfig.json +++ b/src/platform/plugins/shared/telemetry_collection_manager/tsconfig.json @@ -11,7 +11,8 @@ "kbn_references": [ "@kbn/core", "@kbn/usage-collection-plugin", - "@kbn/tracing-utils" + "@kbn/tracing-utils", + "@kbn/apm-utils" ], "exclude": [ "target/**/*", diff --git a/src/platform/plugins/shared/workflows_execution_engine/moon.yml b/src/platform/plugins/shared/workflows_execution_engine/moon.yml index 24d794011b34e..2d281e23922ba 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/moon.yml +++ b/src/platform/plugins/shared/workflows_execution_engine/moon.yml @@ -17,6 +17,7 @@ project: owner: '@elastic/workflows-eng' sourceRoot: src/platform/plugins/shared/workflows_execution_engine dependsOn: + - '@kbn/apm-utils' - '@kbn/core' - '@kbn/config-schema' - '@kbn/actions-plugin' diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/tests/workflow_execution_runtime_manager.test.ts b/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/tests/workflow_execution_runtime_manager.test.ts index 9d050babe923d..d8d1290f5a695 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/tests/workflow_execution_runtime_manager.test.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/tests/workflow_execution_runtime_manager.test.ts @@ -250,12 +250,10 @@ describe('WorkflowExecutionRuntimeManager', () => { await underTest.start(); - expect(mockTransaction.addLabels).toHaveBeenCalledWith( - expect.objectContaining({ - triggered_by: 'task_manager', - event_trigger_id: 'cases.caseCreated', - }) - ); + expect(mockTransaction.addLabels.mock.calls[0][0]).toMatchObject({ + triggered_by: 'task_manager', + event_trigger_id: 'cases.caseCreated', + }); expect(mockTransaction.addLabels.mock.calls[0][0]).not.toHaveProperty('event_chain_depth'); }); @@ -265,12 +263,10 @@ describe('WorkflowExecutionRuntimeManager', () => { await underTest.start(); - expect(mockTransaction.addLabels).toHaveBeenCalledWith( - expect.objectContaining({ - triggered_by: 'task_manager', - event_trigger_id: 'my.custom.trigger', - }) - ); + expect(mockTransaction.addLabels.mock.calls[0][0]).toMatchObject({ + triggered_by: 'task_manager', + event_trigger_id: 'my.custom.trigger', + }); expect(mockTransaction.addLabels.mock.calls[0][0]).not.toHaveProperty('event_chain_depth'); }); diff --git a/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/workflow_execution_runtime_manager.ts b/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/workflow_execution_runtime_manager.ts index 59f1bd0e22f95..c0927fecd9383 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/workflow_execution_runtime_manager.ts +++ b/src/platform/plugins/shared/workflows_execution_engine/server/workflow_context_manager/workflow_execution_runtime_manager.ts @@ -11,6 +11,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import agent from 'elastic-apm-node'; +import { addTransactionLabels } from '@kbn/apm-utils'; import type { CoreStart } from '@kbn/core/server'; import type { EsWorkflowExecution, StackFrame } from '@kbn/workflows'; import { @@ -353,8 +354,9 @@ export class WorkflowExecutionRuntimeManager { this.workflowTransaction = workflowTransaction; - // Add workflow-specific labels - workflowTransaction.addLabels({ + (agent as any).setCurrentTransaction(workflowTransaction); + + addTransactionLabels({ workflow_execution_id: this.workflowExecution.id, workflow_id: this.workflowExecution.workflowId, service_name: 'kibana', @@ -363,9 +365,6 @@ export class WorkflowExecutionRuntimeManager { parent_alerting_rule_id: (existingTransaction as any)._labels?.alerting_rule_id, }); - // Make the workflow transaction the current transaction for subsequent spans - (agent as any).setCurrentTransaction(workflowTransaction); - // Store the workflow transaction ID (not the alerting transaction ID) const workflowTransactionId = workflowTransaction.ids?.['transaction.id']; if (workflowTransactionId) { @@ -417,8 +416,7 @@ export class WorkflowExecutionRuntimeManager { taskManagerLabels.event_trigger_id = triggeredBy; } - // Add workflow-specific labels to the existing transaction (additive; keep triggered_by: task_manager) - existingTransaction.addLabels(taskManagerLabels); + addTransactionLabels(taskManagerLabels); // Store the task transaction ID in the workflow execution const taskTransactionId = existingTransaction.ids?.['transaction.id']; diff --git a/src/platform/plugins/shared/workflows_execution_engine/tsconfig.json b/src/platform/plugins/shared/workflows_execution_engine/tsconfig.json index a5c6695e9aac5..4b7755b3d124b 100644 --- a/src/platform/plugins/shared/workflows_execution_engine/tsconfig.json +++ b/src/platform/plugins/shared/workflows_execution_engine/tsconfig.json @@ -13,6 +13,7 @@ "integration_tests/**/*" ], "kbn_references": [ + "@kbn/apm-utils", "@kbn/core", "@kbn/config-schema", "@kbn/actions-plugin", diff --git a/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts b/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts index a563d23e35882..03873939e7302 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/action_executor.ts @@ -14,7 +14,7 @@ import { } from '@kbn/core/server'; import { cloneDeep, startsWith } from 'lodash'; import { set } from '@kbn/safer-lodash-set'; -import { withSpan } from '@kbn/apm-utils'; +import { addSpanLabels, withSpan } from '@kbn/apm-utils'; import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; import type { SpacesServiceStart } from '@kbn/spaces-plugin/server'; import type { IEventLogger } from '@kbn/event-log-plugin/server'; @@ -497,7 +497,7 @@ export class ActionExecutor { if (span) { span.name = `${executeLabel} ${actionTypeId}`; - span.addLabels({ + addSpanLabels({ actions_connector_type_id: actionTypeId, }); } diff --git a/x-pack/platform/plugins/shared/alerting/server/task_runner/ad_hoc_task_runner.ts b/x-pack/platform/plugins/shared/alerting/server/task_runner/ad_hoc_task_runner.ts index 5d3ff4631cdcc..06b94e8c383f2 100644 --- a/x-pack/platform/plugins/shared/alerting/server/task_runner/ad_hoc_task_runner.ts +++ b/x-pack/platform/plugins/shared/alerting/server/task_runner/ad_hoc_task_runner.ts @@ -11,6 +11,7 @@ import type { ISavedObjectsRepository, KibanaRequest, Logger, SavedObject } from import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import type { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; import { createTaskRunError, TaskErrorSource } from '@kbn/task-manager-plugin/server'; +import { addSpanLabels } from '@kbn/apm-utils'; import { nanosToMillis } from '@kbn/event-log-plugin/common'; import type { CancellableTask, RunResult } from '@kbn/task-manager-plugin/server/task'; import { TaskPriority } from '@kbn/task-manager-plugin/server/task'; @@ -430,17 +431,18 @@ export class AdHocTaskRunner implements CancellableTask { if (apm.currentTransaction) { apm.currentTransaction.name = `Execute Backfill for Alerting Rule`; - apm.currentTransaction.addLabels({ - alerting_rule_space_id: spaceId, - alerting_rule_id: rule.id, - alerting_rule_consumer: rule.consumer, - alerting_rule_name: rule.name, - alerting_rule_tags: rule.tags.join(', '), - alerting_rule_type_id: rule.alertTypeId, - alerting_rule_params: JSON.stringify(rule.params), - }); } + addSpanLabels({ + alerting_rule_space_id: spaceId, + alerting_rule_id: rule.id, + alerting_rule_consumer: rule.consumer, + alerting_rule_name: rule.name, + alerting_rule_tags: rule.tags.join(', '), + alerting_rule_type_id: rule.alertTypeId, + alerting_rule_params: JSON.stringify(rule.params), + }); + if (startedAt) { // Capture how long it took for the task to start running after being claimed this.timer.setDuration(TaskRunnerTimerSpan.StartTaskRun, startedAt); diff --git a/x-pack/platform/plugins/shared/alerting/server/task_runner/log_alerts.ts b/x-pack/platform/plugins/shared/alerting/server/task_runner/log_alerts.ts index 9dd32ee437852..b039a656c87a7 100644 --- a/x-pack/platform/plugins/shared/alerting/server/task_runner/log_alerts.ts +++ b/x-pack/platform/plugins/shared/alerting/server/task_runner/log_alerts.ts @@ -5,8 +5,8 @@ * 2.0. */ -import apm from 'elastic-apm-node'; import type { Logger } from '@kbn/core/server'; +import { addSpanLabels } from '@kbn/apm-utils'; import type { Alert } from '../alert'; import { EVENT_LOG_ACTIONS } from '../plugin'; import type { AlertInstanceContext, AlertInstanceState } from '../types'; @@ -52,13 +52,11 @@ export function logAlerts< const activeAlertIds = Object.keys(activeAlerts); const recoveredAlertIds = Object.keys(recoveredAlerts); - if (apm.currentTransaction) { - apm.currentTransaction.addLabels({ - alerting_new_alerts: newAlertIds.length, - alerting_active_alerts: activeAlertIds.length, - alerting_recovered_alerts: recoveredAlertIds.length, - }); - } + addSpanLabels({ + alerting_new_alerts: newAlertIds.length, + alerting_active_alerts: activeAlertIds.length, + alerting_recovered_alerts: recoveredAlertIds.length, + }); if (activeAlertIds.length > 0 && logger.isLevelEnabled('debug')) { logger.debug( diff --git a/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner.ts b/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner.ts index 786569ce04378..b80d560ae5193 100644 --- a/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner.ts +++ b/x-pack/platform/plugins/shared/alerting/server/task_runner/task_runner.ts @@ -10,6 +10,7 @@ import type { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { v4 as uuidv4 } from 'uuid'; import type { ISavedObjectsRepository, Logger } from '@kbn/core/server'; import type { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; +import { addSpanLabels } from '@kbn/apm-utils'; import { nanosToMillis } from '@kbn/event-log-plugin/server'; import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common'; import { ActionScheduler, type RunResult } from './action_scheduler'; @@ -278,15 +279,16 @@ export class TaskRunner< }: RunRuleParams): Promise { if (apm.currentTransaction) { apm.currentTransaction.name = `Execute Alerting Rule: "${rule.name}"`; - apm.currentTransaction.addLabels({ - alerting_rule_consumer: rule.consumer, - alerting_rule_name: rule.name, - alerting_rule_tags: rule.tags.join(', '), - alerting_rule_type_id: rule.alertTypeId, - alerting_rule_params: JSON.stringify(rule.params), - }); } + addSpanLabels({ + alerting_rule_consumer: rule.consumer, + alerting_rule_name: rule.name, + alerting_rule_tags: rule.tags.join(', '), + alerting_rule_type_id: rule.alertTypeId, + alerting_rule_params: JSON.stringify(rule.params), + }); + const { params: { alertId: ruleId, spaceId }, state: { previousStartedAt }, @@ -526,13 +528,14 @@ export class TaskRunner< if (apm.currentTransaction) { apm.currentTransaction.name = `Execute Alerting Rule`; - apm.currentTransaction.addLabels({ - alerting_rule_space_id: spaceId, - alerting_rule_id: ruleId, - plugins: 'alerting', - }); } + addSpanLabels({ + alerting_rule_space_id: spaceId, + alerting_rule_id: ruleId, + plugins: 'alerting', + }); + this.ruleRunning.start(ruleId, this.context.spaceIdToNamespace(spaceId)); this.logger.debug( diff --git a/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/install.ts b/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/install.ts index 153a85a0bc618..99765168b6794 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/install.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/epm/packages/install.ts @@ -22,6 +22,7 @@ import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; import pRetry from 'p-retry'; import type { LicenseType } from '@kbn/licensing-types'; +import { addSpanLabels } from '@kbn/apm-utils'; import type { KibanaAssetReference, @@ -704,7 +705,7 @@ export async function installPackageWithStateMachine(options: { } try { - span?.addLabels({ + addSpanLabels({ packageName: pkgName, packageVersion: pkgVersion, installType, diff --git a/x-pack/platform/plugins/shared/task_manager/server/task_running/task_runner.ts b/x-pack/platform/plugins/shared/task_manager/server/task_running/task_runner.ts index e2d90830e0863..20ca99146e6bf 100644 --- a/x-pack/platform/plugins/shared/task_manager/server/task_running/task_runner.ts +++ b/x-pack/platform/plugins/shared/task_manager/server/task_running/task_runner.ts @@ -14,7 +14,7 @@ import apm from 'elastic-apm-node'; import { withActiveSpan } from '@kbn/tracing-utils'; import { v4 as uuidv4 } from 'uuid'; -import { withSpan } from '@kbn/apm-utils'; +import { addSpanLabels, withSpan } from '@kbn/apm-utils'; import { flow, identity, omit } from 'lodash'; import type { ExecutionContextStart, @@ -564,7 +564,7 @@ export class TaskManagerRunner implements TaskRunner { TASK_MANAGER_TRANSACTION_TYPE_MARK_AS_RUNNING, TASK_MANAGER_TRANSACTION_TYPE ); - apmTrans.addLabels({ entityId: this.taskType }); + addSpanLabels({ entityId: this.taskType }); const now = new Date(); try { diff --git a/x-pack/solutions/observability/plugins/apm/moon.yml b/x-pack/solutions/observability/plugins/apm/moon.yml index f48b8241ed827..7b6cfa2c58aed 100644 --- a/x-pack/solutions/observability/plugins/apm/moon.yml +++ b/x-pack/solutions/observability/plugins/apm/moon.yml @@ -128,6 +128,7 @@ dependsOn: - '@kbn/saved-search-component' - '@kbn/saved-search-plugin' - '@kbn/charts-theme' + - '@kbn/apm-utils' - '@kbn/response-ops-rule-params' - '@kbn/core-http-server-utils' - '@kbn/key-value-metadata-table' diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/apm_routes/register_apm_server_routes.ts b/x-pack/solutions/observability/plugins/apm/server/routes/apm_routes/register_apm_server_routes.ts index a86152fb09631..ee0a899083099 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/apm_routes/register_apm_server_routes.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/apm_routes/register_apm_server_routes.ts @@ -34,6 +34,7 @@ import apm from 'elastic-apm-node'; import type { VersionedRouteRegistrar } from '@kbn/core-http-server'; import type { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; import type { APMIndices } from '@kbn/apm-sources-access-plugin/server'; +import { addSpanLabels } from '@kbn/apm-utils'; import type { ApmFeatureFlags } from '../../../common/apm_feature_flags'; import type { APMCore, @@ -100,7 +101,7 @@ export function registerRoutes({ response: KibanaResponseFactory ) => { if (agent.isStarted()) { - agent.addLabels({ + addSpanLabels({ plugin: 'apm', }); } diff --git a/x-pack/solutions/observability/plugins/apm/tsconfig.json b/x-pack/solutions/observability/plugins/apm/tsconfig.json index a6009378f296a..1aee996b2c4a4 100644 --- a/x-pack/solutions/observability/plugins/apm/tsconfig.json +++ b/x-pack/solutions/observability/plugins/apm/tsconfig.json @@ -127,6 +127,7 @@ "@kbn/saved-search-component", "@kbn/saved-search-plugin", "@kbn/charts-theme", + "@kbn/apm-utils", "@kbn/response-ops-rule-params", "@kbn/core-http-server-utils", "@kbn/key-value-metadata-table", diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts index 10e19cd769daa..5f8e0872faef8 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts @@ -17,6 +17,7 @@ import type { InterruptValue, TraceData, } from '@kbn/elastic-assistant-common'; +import { addSpanLabels } from '@kbn/apm-utils'; import type { APMTracer } from '@kbn/langchain/server/tracers/apm'; import type { AIMessageChunk } from '@langchain/core/messages'; import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server'; @@ -222,7 +223,7 @@ export const invokeGraph = async ({ transactionId: span.transaction.ids['transaction.id'], traceId: span.ids['trace.id'], }; - span.addLabels({ evaluationId: traceOptions?.evaluationId }); + addSpanLabels({ evaluationId: traceOptions?.evaluationId }); } const result = await assistantGraph.invoke(inputs, { callbacks: [ diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts index 30c1e384dec8b..6770e3f3673e3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts @@ -6,8 +6,8 @@ */ import { omitBy } from 'lodash'; -import agent from 'elastic-apm-node'; import type { Logger } from '@kbn/core/server'; +import { addSpanLabels } from '@kbn/apm-utils'; import type { PublicRuleMonitoringService, PublicRuleResultService, @@ -157,7 +157,7 @@ export function createRuleExecutionLogClientForExecutors( const correlationIds = baseCorrelationIds.withStatus(executionResult.status); const logMeta = correlationIds.getLogMeta(); - agent.addLabels({ [SECURITY_RULE_STATUS]: executionResult.status }); + addSpanLabels({ [SECURITY_RULE_STATUS]: executionResult.status }); try { const normalizedExecutionResult: ExecutionResult = { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index f95171b11f917..9bbf0d03a1443 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -9,6 +9,7 @@ import { partition, sum } from 'lodash'; import agent from 'elastic-apm-node'; import type { estypes } from '@elastic/elasticsearch'; +import { addSpanLabels } from '@kbn/apm-utils'; import { TIMESTAMP } from '@kbn/rule-data-utils'; import { createPersistenceRuleTypeWrapper } from '@kbn/rule-registry-plugin/server'; import { buildExceptionFilter } from '@kbn/lists-plugin/server/services/exception_lists'; @@ -69,7 +70,7 @@ Object.entries(aadFieldConversion).forEach(([key, value]) => { }); const addApmLabelsFromParams = (params: RuleParams) => { - agent.addLabels( + addSpanLabels( { [SECURITY_FROM]: params.from, [SECURITY_IMMUTABLE]: params.immutable, @@ -77,7 +78,7 @@ const addApmLabelsFromParams = (params: RuleParams) => { [SECURITY_RULE_ID]: params.ruleId, [SECURITY_TO]: params.to, }, - false + { isString: false } ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts index 89484883d4007..53122af200f92 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/detection_engine/rule_types/utils/utils.ts @@ -13,6 +13,7 @@ import objectHash from 'object-hash'; import dateMath from '@kbn/datemath'; import type { estypes, TransportResult } from '@elastic/elasticsearch'; +import { addSpanLabels } from '@kbn/apm-utils'; import { ALERT_UUID, ALERT_RULE_UUID, @@ -306,7 +307,7 @@ export const getGapBetweenRuns = ({ return moment.duration(0); } const driftTolerance = moment.duration(originalTo.diff(originalFrom)); - agent.addLabels({ [SECURITY_QUERY_SPAN_S]: driftTolerance.asSeconds() }, false); + addSpanLabels({ [SECURITY_QUERY_SPAN_S]: driftTolerance.asSeconds() }, { isString: false }); const currentDuration = moment.duration(moment(startedAt).diff(previousStartedAt)); return currentDuration.subtract(driftTolerance); };