From 33afa9876f9e2ce4f898ca06061eddc14cccc54e Mon Sep 17 00:00:00 2001 From: samunohito <46447427+samunohito@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:41:21 +0900 Subject: [PATCH] fix CodeRabbit review --- packages/backend/package.json | 2 +- packages/backend/src/config.ts | 15 +++- .../telemetry/http-client-instrumentation.ts | 74 +++++++++++------ .../core/telemetry/redis-instrumentation.ts | 39 ++++++--- .../src/core/telemetry/telemetry-registry.ts | 5 +- .../e2e/telemetry-redis-instrumentation.ts | 3 +- .../adapters/SentryTelemetryAdapter.ts | 54 ++++++++---- .../telemetry/http-client-instrumentation.ts | 82 ++++++++++++++++++- .../core/telemetry/queue-trace-context.ts | 7 +- .../unit/telemetry-redis-instrumentation.ts | 61 ++++++++++++++ .../backend/test/unit/telemetry-registry.ts | 22 +++++ pnpm-lock.yaml | 4 +- pnpm-workspace.yaml | 2 - 13 files changed, 307 insertions(+), 63 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 1adbde0827..4476f9f08d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -71,7 +71,7 @@ "@opentelemetry/api": "1.9.1", "@opentelemetry/core": "2.9.0", "@opentelemetry/exporter-trace-otlp-proto": "0.220.0", - "@opentelemetry/instrumentation": "0.219.0", + "@opentelemetry/instrumentation": "0.220.0", "@opentelemetry/instrumentation-pg": "0.72.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/sdk-trace-base": "2.9.0", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 66540ae1d1..74a8d9d40e 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -27,6 +27,10 @@ type SentryBackendConfig = { disabledIntegrations?: string[]; }; +type SentryBackendConfigSource = Omit & { + options?: Partial; +}; + type OtelBackendConfig = { endpoint?: string; headers?: Record; @@ -86,7 +90,7 @@ type Source = { index: string; scope?: 'local' | 'global' | string[]; }; - sentryForBackend?: SentryBackendConfig; + sentryForBackend?: SentryBackendConfigSource; otelForBackend?: OtelBackendConfig; sentryForFrontend?: { options: Partial & { dsn: string }; @@ -338,7 +342,7 @@ export function loadConfig(): Config { redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis, redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis, redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis, - sentryForBackend: config.sentryForBackend, + sentryForBackend: config.sentryForBackend == null ? undefined : normalizeSentryBackendConfig(config.sentryForBackend), otelForBackend: config.otelForBackend, sentryForFrontend: config.sentryForFrontend, id: config.id, @@ -376,6 +380,13 @@ export function loadConfig(): Config { }; } +export function normalizeSentryBackendConfig(config: SentryBackendConfigSource): SentryBackendConfig { + return { + ...config, + options: config.options ?? {}, + }; +} + function tryCreateUrl(url: string) { try { return new URL(url); diff --git a/packages/backend/src/core/telemetry/http-client-instrumentation.ts b/packages/backend/src/core/telemetry/http-client-instrumentation.ts index a2ae4b5692..f2999042f9 100644 --- a/packages/backend/src/core/telemetry/http-client-instrumentation.ts +++ b/packages/backend/src/core/telemetry/http-client-instrumentation.ts @@ -4,6 +4,7 @@ */ import { channel } from 'node:diagnostics_channel'; +import { diag } from '@opentelemetry/api'; import type { ClientRequest, IncomingMessage } from 'node:http'; import type { Span, SpanOptions, SpanStatusCode, Tracer } from '@opentelemetry/api'; @@ -18,6 +19,7 @@ type HttpClientInstrumentationDeps = { spanKindClient: SpanOptions['kind']; spanStatusCodeError: SpanStatusCode; subscribe: (name: string, listener: (message: unknown) => void) => () => void; + reportError?: (error: unknown) => void; }; type RequestCreatedMessage = { request: ClientRequest }; @@ -31,10 +33,10 @@ type RequestErrorMessage = { request: ClientRequest; error: Error }; export function createHttpClientInstrumentation(deps: HttpClientInstrumentationDeps): () => void { const spans = new WeakMap(); - const unsubscribeCreated = deps.subscribe(HTTP_CLIENT_REQUEST_CREATED, (message: unknown) => { + const unsubscribeCreated = deps.subscribe(HTTP_CLIENT_REQUEST_CREATED, guardDiagnosticsListener(deps, (message: unknown) => { const { request } = message as RequestCreatedMessage; const { url, host, port } = getRequestDetails(request); - const method = request.method ?? 'GET'; + const method = request.method; const span = deps.tracer.startSpan(method, { kind: deps.spanKindClient, attributes: { @@ -45,9 +47,9 @@ export function createHttpClientInstrumentation(deps: HttpClientInstrumentationD }, }); spans.set(request, span); - }); + })); - const unsubscribeResponseFinish = deps.subscribe(HTTP_CLIENT_RESPONSE_FINISH, (message: unknown) => { + const unsubscribeResponseFinish = deps.subscribe(HTTP_CLIENT_RESPONSE_FINISH, guardDiagnosticsListener(deps, (message: unknown) => { const { request, response } = message as ResponseFinishMessage; const span = spans.get(request); if (span == null) return; @@ -56,18 +58,16 @@ export function createHttpClientInstrumentation(deps: HttpClientInstrumentationD if (statusCode != null) { span.setAttribute('http.response.status_code', statusCode); } - if (response.httpVersion != null) { - span.setAttribute('network.protocol.version', response.httpVersion); - } + span.setAttribute('network.protocol.version', response.httpVersion); if (statusCode != null && statusCode >= 400) { span.setAttribute('error.type', String(statusCode)); span.setStatus({ code: deps.spanStatusCodeError }); } span.end(); spans.delete(request); - }); + })); - const unsubscribeRequestError = deps.subscribe(HTTP_CLIENT_REQUEST_ERROR, (message: unknown) => { + const unsubscribeRequestError = deps.subscribe(HTTP_CLIENT_REQUEST_ERROR, guardDiagnosticsListener(deps, (message: unknown) => { const { request, error } = message as RequestErrorMessage; const span = spans.get(request); if (span == null) return; @@ -77,7 +77,7 @@ export function createHttpClientInstrumentation(deps: HttpClientInstrumentationD span.setStatus({ code: deps.spanStatusCodeError }); span.end(); spans.delete(request); - }); + })); return () => { unsubscribeCreated(); @@ -89,6 +89,7 @@ export function createHttpClientInstrumentation(deps: HttpClientInstrumentationD export function installHttpClientInstrumentation(deps: Omit): () => void { return createHttpClientInstrumentation({ ...deps, + reportError: deps.reportError ?? (error => diag.error('Failed to process HTTP client diagnostics event.', error)), subscribe: (name, listener) => { const diagnosticChannel = channel(name); diagnosticChannel.subscribe(listener); @@ -98,20 +99,47 @@ export function installHttpClientInstrumentation(deps: Omit, + listener: (message: unknown) => void, +): (message: unknown) => void { + return (message) => { + try { + listener(message); + } catch (error) { + try { + deps.reportError?.(error); + } catch { + // diagnostics listener の失敗をアプリケーションへ伝播させない。 + } + } }; } diff --git a/packages/backend/src/core/telemetry/redis-instrumentation.ts b/packages/backend/src/core/telemetry/redis-instrumentation.ts index b3e8d80b39..cb7dd268ab 100644 --- a/packages/backend/src/core/telemetry/redis-instrumentation.ts +++ b/packages/backend/src/core/telemetry/redis-instrumentation.ts @@ -4,13 +4,13 @@ */ import { tracingChannel } from 'node:diagnostics_channel'; -import { context, trace } from '@opentelemetry/api'; +import { context, diag, trace } from '@opentelemetry/api'; import type { Span, SpanKind, SpanStatusCode, Tracer } from '@opentelemetry/api'; type IORedisCommandContext = { command: string; args: string[]; - database: number; + database: number | undefined; serverAddress: string; serverPort: number | undefined; }; @@ -39,6 +39,7 @@ type RedisInstrumentationDeps = { getActiveSpan(): Span | undefined; spanKindClient: SpanKind; spanStatusCodeError: SpanStatusCode; + reportError?: (error: unknown) => void; }; type RedisInstrumentationOptions = { @@ -65,6 +66,7 @@ export function installRedisInstrumentation( getActiveSpan: () => trace.getSpan(context.active()), spanKindClient, spanStatusCodeError, + reportError: error => diag.error('Failed to process Redis diagnostics event.', error), }, options); } @@ -77,9 +79,9 @@ export function createRedisInstrumentation(deps: RedisInstrumentationDeps, optio name: message.command, attributes: { 'db.system.name': 'redis', - 'db.namespace': message.database.toString(10), 'db.operation.name': message.command, 'server.address': message.serverAddress, + ...(message.database != null ? { 'db.namespace': message.database.toString(10) } : {}), ...(message.serverPort != null ? { 'server.port': message.serverPort } : {}), }, })); @@ -138,7 +140,7 @@ function createTracingChannelSubscribers( }; const subscribers: TracingChannelSubscribers = { - start: (message) => { + start: guardTracingSubscriber(deps, (message: T) => { if (requireParentSpan && deps.getActiveSpan() == null) return; const options = getSpanOptions(message); @@ -147,24 +149,41 @@ function createTracingChannelSubscribers( attributes: options.attributes, }); spans.set(message, { span, recordedError: false }); - }, + }), // Promiseを返すコマンドでは完了前にもendイベントが来るため、同期例外だけここで閉じる。 - end: (message) => { + end: guardTracingSubscriber(deps, (message: T & { error?: unknown }) => { if (message.error != null) finish(message); - }, + }), asyncStart: () => {}, - asyncEnd: finish, - error: (message) => { + asyncEnd: guardTracingSubscriber(deps, finish), + error: guardTracingSubscriber(deps, (message: T & { error: unknown }) => { const state = spans.get(message); if (state == null) return; recordError(state, message.error); - }, + }), }; channel.subscribe(subscribers); return subscribers; } +function guardTracingSubscriber( + deps: Pick, + subscriber: (message: T) => void, +): (message: T) => void { + return (message) => { + try { + subscriber(message); + } catch (error) { + try { + deps.reportError?.(error); + } catch { + // diagnostics subscriber の失敗をアプリケーションへ伝播させない。 + } + } + }; +} + function toError(value: unknown): Error { return value instanceof Error ? value : new Error(String(value)); } diff --git a/packages/backend/src/core/telemetry/telemetry-registry.ts b/packages/backend/src/core/telemetry/telemetry-registry.ts index 94e26e21d8..a8cd4c942b 100644 --- a/packages/backend/src/core/telemetry/telemetry-registry.ts +++ b/packages/backend/src/core/telemetry/telemetry-registry.ts @@ -73,7 +73,10 @@ export function startSpanWithTraceContext(name: string, jobData: object, fn: // Queue context を解釈できる adapter に span 作成を任せる。 // 対応 adapter が無い構成では通常の startSpan へフォールバックする。 const adapter = adapters.find(adapter => adapter.startSpanWithTraceContext != null); - return adapter?.startSpanWithTraceContext?.(name, jobData, fn) ?? startSpan(name, fn); + if (adapter?.startSpanWithTraceContext != null) { + return adapter.startSpanWithTraceContext(name, jobData, fn); + } + return startSpan(name, fn); } export async function shutdownTelemetry(): Promise { diff --git a/packages/backend/test/e2e/telemetry-redis-instrumentation.ts b/packages/backend/test/e2e/telemetry-redis-instrumentation.ts index 620f6adc69..92b9fa4651 100644 --- a/packages/backend/test/e2e/telemetry-redis-instrumentation.ts +++ b/packages/backend/test/e2e/telemetry-redis-instrumentation.ts @@ -55,6 +55,7 @@ describe('Redis telemetry instrumentation', () => { }, { connection, prefix }); worker.once('completed', () => resolve()); worker.once('failed', (_job, error) => reject(error)); + worker.on('error', reject); }); await tracer.startActiveSpan('HTTP POST /telemetry-test', async httpSpan => { @@ -78,7 +79,7 @@ describe('Redis telemetry instrumentation', () => { expect(Object.values(span.attributes)).not.toContain(secret); } } finally { - await worker?.close(); + await worker?.close(true); await queue.obliterate({ force: true }); await queue.close(); uninstall(); diff --git a/packages/backend/test/unit/core/telemetry/adapters/SentryTelemetryAdapter.ts b/packages/backend/test/unit/core/telemetry/adapters/SentryTelemetryAdapter.ts index 9423aebe51..cfe2b3dd81 100644 --- a/packages/backend/test/unit/core/telemetry/adapters/SentryTelemetryAdapter.ts +++ b/packages/backend/test/unit/core/telemetry/adapters/SentryTelemetryAdapter.ts @@ -4,9 +4,11 @@ */ import { describe, expect, test, vi } from 'vitest'; +import { normalizeSentryBackendConfig } from '@/config.js'; import { SentryTelemetryAdapter, buildSentryIntegrations, buildSentryNodeOptions, buildSentryOtlpInitOptions } from '@/core/telemetry/adapters/SentryTelemetryAdapter.js'; type TestIntegration = Parameters>[0][number]; +type TestSpanProcessor = NonNullable['openTelemetrySpanProcessors']>[number]; function testIntegration(name: string): TestIntegration { return { name }; @@ -79,43 +81,59 @@ describe('SentryTelemetryAdapter', () => { }); test('builds Sentry options that export spans to both Sentry and OTLP', () => { - const existingProcessor = { name: 'existingProcessor' }; + const existingProcessor = { name: 'existingProcessor' } as unknown as TestSpanProcessor; const otlpProcessor = { name: 'otlpProcessor' }; - const result = buildSentryOtlpInitOptions({ - sentryConfig: { - enableNodeProfiling: false, - disabledIntegrations: ['Redis'], + const result = buildSentryOtlpInitOptions({ + sentryConfig: { + enableNodeProfiling: false, + disabledIntegrations: ['Redis'], options: { - openTelemetrySpanProcessors: [existingProcessor as any], + openTelemetrySpanProcessors: [existingProcessor], tracesSampleRate: 0.25, - }, }, - otelConfig: { serviceVersion: '2026.1.0' }, - otlpProcessor, - }); + }, + otelConfig: { serviceVersion: '2026.1.0' }, + otlpProcessor, + }); expect(result.tracesSampleRate).toBe(0.25); expect(result.openTelemetrySpanProcessors).toEqual([existingProcessor, otlpProcessor]); // OTel併存時もremoteへtrace headerを漏らさないデフォルトはSentry単体時と揃える。 expect(result.tracePropagationTargets).toEqual([]); - expect((result.integrations as any)([ + expect(result.integrations).toBeTypeOf('function'); + if (typeof result.integrations !== 'function') throw new Error('Expected integrations to be a function'); + expect(result.integrations([ testIntegration('Http'), testIntegration('Redis'), testIntegration('Postgres'), ]).map((integration: TestIntegration) => integration.name)).toEqual(['Http', 'Postgres']); }); + test('uses safe defaults when Sentry options are omitted', () => { + const otlpProcessor = { name: 'otlpProcessor' }; + const result = buildSentryOtlpInitOptions({ + sentryConfig: normalizeSentryBackendConfig({ + enableNodeProfiling: false, + }), + otelConfig: { serviceVersion: '2026.1.0' }, + otlpProcessor, + }); + + expect(result.tracePropagationTargets).toEqual([]); + expect(result.openTelemetrySpanProcessors).toEqual([otlpProcessor]); + }); + test('does not disable Sentry trace propagation when explicitly enabled for OTel coexistence', () => { const result = buildSentryOtlpInitOptions({ sentryConfig: { enableNodeProfiling: false, options: {}, }, - otelConfig: { - serviceVersion: '2026.1.0', - propagateTraceToRemote: true, - }, + otelConfig: { + serviceVersion: '2026.1.0', + propagateTraceToRemote: true, + }, otlpProcessor: { name: 'otlpProcessor' }, }); @@ -130,9 +148,9 @@ describe('SentryTelemetryAdapter', () => { tracePropagationTargets: ['^https://internal\\.example/'], }, }, - otelConfig: { serviceVersion: '2026.1.0' }, - otlpProcessor: { name: 'otlpProcessor' }, - }); + otelConfig: { serviceVersion: '2026.1.0' }, + otlpProcessor: { name: 'otlpProcessor' }, + }); expect(result.tracePropagationTargets).toEqual(['^https://internal\\.example/']); }); diff --git a/packages/backend/test/unit/core/telemetry/http-client-instrumentation.ts b/packages/backend/test/unit/core/telemetry/http-client-instrumentation.ts index e05f4407e4..324bd9005f 100644 --- a/packages/backend/test/unit/core/telemetry/http-client-instrumentation.ts +++ b/packages/backend/test/unit/core/telemetry/http-client-instrumentation.ts @@ -57,7 +57,7 @@ describe('http-client-instrumentation', () => { expect(span.setAttribute).toHaveBeenCalledWith('network.protocol.version', '1.1'); expect(span.end).toHaveBeenCalledTimes(1); unsubscribe(); - expect(listeners).toHaveLength(0); + expect(listeners.size).toBe(0); }); test('records a request error and ends the span once', () => { @@ -105,4 +105,84 @@ describe('http-client-instrumentation', () => { expect(span.setAttribute).toHaveBeenCalledWith('error.type', '502'); expect(span.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.ERROR }); }); + + test('uses safe request details when the request URL cannot be constructed', () => { + const listeners = new Map void>(); + const span = { end: vi.fn(), recordException: vi.fn(), setAttribute: vi.fn(), setStatus: vi.fn() }; + const tracer = { startSpan: vi.fn(() => span) }; + createHttpClientInstrumentation({ + tracer: tracer as any, + spanKindClient: SpanKind.CLIENT, + spanStatusCodeError: SpanStatusCode.ERROR, + subscribe: (name, listener) => { + listeners.set(name, listener); + return () => listeners.delete(name); + }, + }); + const clientRequest = { + ...request(), + host: '::1', + getHeader: vi.fn(() => undefined), + }; + + listeners.get('http.client.request.created')!({ request: clientRequest }); + + expect(tracer.startSpan).toHaveBeenCalledWith('POST', { + kind: SpanKind.CLIENT, + attributes: { + 'http.request.method': 'POST', + 'url.full': 'https://localhost/', + 'server.address': 'localhost', + 'server.port': 443, + }, + }); + }); + + test('does not propagate errors from diagnostics listeners', () => { + const listeners = new Map void>(); + const reportError = vi.fn(); + const responseError = new Error('response instrumentation failed'); + const requestError = new Error('request instrumentation failed'); + const startError = new Error('span creation failed'); + const span = { + end: vi.fn(), + recordException: vi.fn(() => { throw requestError; }), + setAttribute: vi.fn(() => { throw responseError; }), + setStatus: vi.fn(), + }; + const tracer = { + startSpan: vi.fn() + .mockReturnValueOnce(span) + .mockReturnValueOnce(span) + .mockImplementationOnce(() => { throw startError; }), + }; + createHttpClientInstrumentation({ + tracer: tracer as any, + spanKindClient: SpanKind.CLIENT, + spanStatusCodeError: SpanStatusCode.ERROR, + subscribe: (name, listener) => { + listeners.set(name, listener); + return () => listeners.delete(name); + }, + reportError, + }); + const responseRequest = request(); + const errorRequest = request(); + + listeners.get('http.client.request.created')!({ request: responseRequest }); + expect(() => listeners.get('http.client.response.finish')!({ + request: responseRequest, + response: { statusCode: 200 }, + })).not.toThrow(); + listeners.get('http.client.request.created')!({ request: errorRequest }); + expect(() => listeners.get('http.client.request.error')!({ + request: errorRequest, + error: new Error('connection failed'), + })).not.toThrow(); + expect(() => listeners.get('http.client.request.created')!({ request: request() })).not.toThrow(); + + expect(reportError).toHaveBeenCalledWith(responseError); + expect(reportError).toHaveBeenCalledWith(requestError); + expect(reportError).toHaveBeenCalledWith(startError); + }); }); diff --git a/packages/backend/test/unit/core/telemetry/queue-trace-context.ts b/packages/backend/test/unit/core/telemetry/queue-trace-context.ts index 3ecf4fe270..680cafc87d 100644 --- a/packages/backend/test/unit/core/telemetry/queue-trace-context.ts +++ b/packages/backend/test/unit/core/telemetry/queue-trace-context.ts @@ -7,8 +7,8 @@ import { describe, expect, test, vi } from 'vitest'; import type { Context, SpanContext } from '@opentelemetry/api'; import { getQueueSpanContext, getQueueTraceContextMode, injectActiveTraceContext, injectQueueTraceContext } from '@/core/telemetry/queue-trace-context.js'; -const rootContext = {} as Context; -const extractedContext = {} as Context; +const rootContext = { kind: 'root' } as unknown as Context; +const extractedContext = { kind: 'extracted' } as unknown as Context; const sourceSpanContext: SpanContext = { traceId: '0123456789abcdef0123456789abcdef', spanId: '0123456789abcdef', @@ -94,6 +94,8 @@ describe('queue-trace-context', () => { }, parentContext: rootContext, }); + expect(result?.parentContext).toBe(rootContext); + expect(getSpanContext).toHaveBeenCalledWith(extractedContext); }); test('uses the extracted context as the parent when parent mode is selected', () => { @@ -108,6 +110,7 @@ describe('queue-trace-context', () => { options: {}, parentContext: extractedContext, }); + expect(result?.parentContext).toBe(extractedContext); }); test('ignores malformed or missing carriers', () => { diff --git a/packages/backend/test/unit/telemetry-redis-instrumentation.ts b/packages/backend/test/unit/telemetry-redis-instrumentation.ts index 85a7df3dca..1bbfe5e0f5 100644 --- a/packages/backend/test/unit/telemetry-redis-instrumentation.ts +++ b/packages/backend/test/unit/telemetry-redis-instrumentation.ts @@ -145,4 +145,65 @@ describe('redis-instrumentation', () => { expect(tracingChannel).not.toHaveBeenCalled(); }); + + test('omits the database attribute when diagnostics context has no database', () => { + let subscribers: any; + const span = { end: vi.fn(), recordException: vi.fn(), setStatus: vi.fn(), setAttribute: vi.fn() }; + const tracer = { startSpan: vi.fn(() => span) }; + createRedisInstrumentation({ + tracingChannel: () => ({ subscribe: (value) => { subscribers = value; }, unsubscribe: vi.fn() }), + tracer: tracer as any, + getActiveSpan: () => ({}) as any, + spanKindClient: SpanKind.CLIENT, + spanStatusCodeError: SpanStatusCode.ERROR, + }, { captureCommandSpans: true }); + + subscribers.start({ command: 'get', args: ['key'], database: undefined, serverAddress: 'redis', serverPort: 6379 }); + + expect(tracer.startSpan).toHaveBeenCalledWith('get', expect.objectContaining({ + attributes: expect.not.objectContaining({ + 'db.namespace': expect.anything(), + }), + })); + }); + + test('does not propagate errors from Redis tracing subscribers', () => { + const subscribers = new Map(); + const reportError = vi.fn(); + const endError = new Error('span end failed'); + const startError = new Error('span creation failed'); + const span = { + end: vi.fn(() => { throw endError; }), + recordException: vi.fn(), + setStatus: vi.fn(), + setAttribute: vi.fn(), + }; + const tracer = { + startSpan: vi.fn() + .mockReturnValueOnce(span) + .mockImplementationOnce(() => { throw startError; }), + }; + createRedisInstrumentation({ + tracingChannel: (name) => ({ + subscribe: (value) => { subscribers.set(name, value); }, + unsubscribe: vi.fn(), + }), + tracer: tracer as any, + getActiveSpan: () => ({}) as any, + spanKindClient: SpanKind.CLIENT, + spanStatusCodeError: SpanStatusCode.ERROR, + reportError, + }, { + captureCommandSpans: true, + captureConnectionSpans: true, + }); + const command = { command: 'get', args: ['key'], database: 0, serverAddress: 'redis', serverPort: 6379 }; + + subscribers.get('ioredis:command').start(command); + expect(() => subscribers.get('ioredis:command').asyncEnd(command)).not.toThrow(); + expect(() => subscribers.get('ioredis:connect').start({ serverAddress: 'redis', serverPort: 6379 })).not.toThrow(); + + expect(reportError).toHaveBeenCalledWith(endError); + expect(reportError).toHaveBeenCalledWith(startError); + }); }); diff --git a/packages/backend/test/unit/telemetry-registry.ts b/packages/backend/test/unit/telemetry-registry.ts index dbf10c9176..bb13c6b730 100644 --- a/packages/backend/test/unit/telemetry-registry.ts +++ b/packages/backend/test/unit/telemetry-registry.ts @@ -127,6 +127,28 @@ describe('telemetry-registry', () => { expect(adapterStartSpan).toHaveBeenCalledWith('test', fn); }); + test('startSpanWithTraceContext executes an undefined-returning callback only once', async () => { + const { initTelemetry, startSpanWithTraceContext } = await import('@/core/telemetry/telemetry-registry.js'); + const otelForBackend = { endpoint: 'http://collector:4318/v1/traces' }; + const adapterStartSpan = vi.fn((_name: string, fn: () => undefined) => fn()); + const adapterStartSpanWithTraceContext = vi.fn((_name: string, _jobData: object, fn: () => undefined) => fn()); + mocks.otelCreate.mockResolvedValue({ + shutdown: vi.fn(), + captureMessage: vi.fn(), + startSpan: adapterStartSpan, + startSpanWithTraceContext: adapterStartSpanWithTraceContext, + }); + + await initTelemetry(config({ otelForBackend })); + + const jobData = { id: 'job' }; + const fn = vi.fn(() => undefined); + expect(startSpanWithTraceContext('test', jobData, fn)).toBeUndefined(); + expect(adapterStartSpanWithTraceContext).toHaveBeenCalledWith('test', jobData, fn); + expect(adapterStartSpan).not.toHaveBeenCalled(); + expect(fn).toHaveBeenCalledTimes(1); + }); + test('startSpan wraps work through multiple registered adapters in order for future adapter combinations', async () => { const { initTelemetry, startSpan } = await import('@/core/telemetry/telemetry-registry.js'); const calls: string[] = []; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bff769483..cd060b954d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -231,8 +231,8 @@ importers: specifier: 0.220.0 version: 0.220.0(@opentelemetry/api@1.9.1) '@opentelemetry/instrumentation': - specifier: 0.219.0 - version: 0.219.0(@opentelemetry/api@1.9.1) + specifier: 0.220.0 + version: 0.220.0(@opentelemetry/api@1.9.1) '@opentelemetry/instrumentation-pg': specifier: 0.72.0 version: 0.72.0(@opentelemetry/api@1.9.1) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 55e7b614ea..df968dfd9f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -54,8 +54,6 @@ minimumReleaseAgeExclude: - slacc-win32-x64-msvc - '@typescript/native-preview*' - vite # そのうち消す - # Renovate security update: @opentelemetry/core@2.8.0 - - "@opentelemetry/core@2.8.0" # Renovate security update: typeorm@1.1.0 - typeorm@1.1.0 # Renovate security update: @fastify/static@10.1.2