From efe870cefbec0375e8488196c50d1d45e271ccf4 Mon Sep 17 00:00:00 2001 From: adcoelho Date: Wed, 4 Mar 2026 14:16:15 +0100 Subject: [PATCH] Fix linting problem in apm middleware --- .../middleware/apm_middleware.test.ts | 51 +++++++++++-------- .../middleware/apm_middleware.ts | 23 ++++++--- .../plugins/shared/alerting_v2/tsconfig.json | 1 + 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/rule_executor/middleware/apm_middleware.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/rule_executor/middleware/apm_middleware.test.ts index 522da7021b19d..2b6bcdc00b52d 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/rule_executor/middleware/apm_middleware.test.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/rule_executor/middleware/apm_middleware.test.ts @@ -5,32 +5,39 @@ * 2.0. */ -import agent from 'elastic-apm-node'; +import { SpanStatusCode } from '@opentelemetry/api'; import { ApmMiddleware } from './apm_middleware'; import { createRuleExecutionMiddlewareContext } from './test_utils'; import { collectStreamResults, createPipelineStream, createRulePipelineState } from '../test_utils'; +import { getDefaultTracer } from '@kbn/default-tracer'; -jest.mock('elastic-apm-node', () => ({ - startSpan: jest.fn(), +const mockSpan = { + setStatus: jest.fn(), + recordException: jest.fn(), + end: jest.fn(), + isRecording: jest.fn().mockReturnValue(true), +}; + +const mockStartSpan = jest.fn(() => mockSpan); + +jest.mock('@kbn/default-tracer', () => ({ + getDefaultTracer: jest.fn(() => ({ + startSpan: mockStartSpan, + })), })); -const agentMock = agent as jest.Mocked; +const getDefaultTracerMock = getDefaultTracer as jest.MockedFunction; describe('ApmMiddleware', () => { let middleware: ApmMiddleware; - let mockSpan: { addLabels: jest.Mock; outcome: string | undefined; end: jest.Mock }; beforeEach(() => { middleware = new ApmMiddleware(); - mockSpan = { addLabels: jest.fn(), outcome: undefined, end: jest.fn() }; - agentMock.startSpan.mockReturnValue(mockSpan as never); - }); - - afterEach(() => { jest.clearAllMocks(); + mockSpan.isRecording.mockReturnValue(true); }); - it('wraps the stream in an APM span and sets success outcome', async () => { + it('wraps the stream in an OpenTelemetry span and sets success status', async () => { const state = createRulePipelineState(); const context = createRuleExecutionMiddlewareContext(); const next = jest.fn().mockReturnValue(createPipelineStream([state])); @@ -40,13 +47,14 @@ describe('ApmMiddleware', () => { ); expect(results).toEqual([{ type: 'continue', state }]); - expect(agentMock.startSpan).toHaveBeenCalledWith('rule_executor:test_step', 'rule_executor'); - expect(mockSpan.addLabels).toHaveBeenCalledWith({ plugin: 'alerting_v2' }); - expect(mockSpan.outcome).toBe('success'); + expect(mockStartSpan).toHaveBeenCalledWith('rule_executor:test_step', { + attributes: { plugin: 'alerting_v2' }, + }); + expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK }); expect(mockSpan.end).toHaveBeenCalledTimes(1); }); - it('sets failure outcome and ends span when stream throws', async () => { + it('sets error status and ends span when stream throws', async () => { const context = createRuleExecutionMiddlewareContext(); const error = new Error('stream error'); @@ -60,7 +68,11 @@ describe('ApmMiddleware', () => { collectStreamResults(middleware.execute(context, next, createPipelineStream())) ).rejects.toThrow('stream error'); - expect(mockSpan.outcome).toBe('failure'); + expect(mockSpan.recordException).toHaveBeenCalledWith(error); + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.ERROR, + message: 'stream error', + }); expect(mockSpan.end).toHaveBeenCalledTimes(1); }); @@ -75,12 +87,12 @@ describe('ApmMiddleware', () => { ); expect(results).toHaveLength(2); - expect(mockSpan.outcome).toBe('success'); + expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK }); expect(mockSpan.end).toHaveBeenCalledTimes(1); }); - it('handles null span gracefully when agent is not started', async () => { - agentMock.startSpan.mockReturnValue(null as never); + it('handles undefined tracer gracefully when tracer is not available', async () => { + getDefaultTracerMock.mockReturnValueOnce(undefined as never); const state = createRulePipelineState(); const context = createRuleExecutionMiddlewareContext(); @@ -91,7 +103,6 @@ describe('ApmMiddleware', () => { ); expect(results).toEqual([{ type: 'continue', state }]); - expect(mockSpan.addLabels).not.toHaveBeenCalled(); expect(mockSpan.end).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/rule_executor/middleware/apm_middleware.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/rule_executor/middleware/apm_middleware.ts index 36f05d33ae730..cee0d8e8ffbf4 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/rule_executor/middleware/apm_middleware.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/rule_executor/middleware/apm_middleware.ts @@ -5,14 +5,15 @@ * 2.0. */ -import agent from 'elastic-apm-node'; +import { SpanStatusCode } from '@opentelemetry/api'; +import { getDefaultTracer } from '@kbn/default-tracer'; import { injectable } from 'inversify'; import type { RuleExecutionMiddlewareContext, RuleExecutionMiddleware } from './types'; import type { PipelineStateStream } from '../types'; import { APP_ID } from '../../constants'; /** - * Middleware that wraps each step's stream processing in an APM span for tracing. + * Middleware that wraps each step's stream processing in an OpenTelemetry span for tracing. * * The span stays open for the entire duration of the step's stream, * capturing both success and failure outcomes. @@ -27,22 +28,28 @@ export class ApmMiddleware implements RuleExecutionMiddleware { input: PipelineStateStream ): PipelineStateStream { const stream = next(input); + const tracer = getDefaultTracer(); return (async function* () { - const span = agent.startSpan(`rule_executor:${ctx.step.name}`, 'rule_executor') ?? undefined; - span?.addLabels({ plugin: APP_ID }); + const span = tracer?.startSpan(`rule_executor:${ctx.step.name}`, { + attributes: { plugin: APP_ID }, + }); try { for await (const result of stream) { yield result; } - if (span) { - span.outcome = 'success'; + if (span?.isRecording()) { + span.setStatus({ code: SpanStatusCode.OK }); } } catch (error) { - if (span) { - span.outcome = 'failure'; + if (span?.isRecording()) { + span.recordException(error as Error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error instanceof Error ? error.message : String(error), + }); } throw error; diff --git a/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json b/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json index 82326baa23da2..f990d69a50a73 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json +++ b/x-pack/platform/plugins/shared/alerting_v2/tsconfig.json @@ -49,6 +49,7 @@ "@kbn/test", "@kbn/core-saved-objects-api-server-mocks", "@kbn/apm-utils", + "@kbn/default-tracer", "@kbn/core-user-profile-common", "@kbn/core-user-profile-server-mocks", "@kbn/core-user-profile-server",