mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-31 20:35:52 +00:00
fix CodeRabbit review
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -27,6 +27,10 @@ type SentryBackendConfig = {
|
||||
disabledIntegrations?: string[];
|
||||
};
|
||||
|
||||
type SentryBackendConfigSource = Omit<SentryBackendConfig, 'options'> & {
|
||||
options?: Partial<Sentry.NodeOptions>;
|
||||
};
|
||||
|
||||
type OtelBackendConfig = {
|
||||
endpoint?: string;
|
||||
headers?: Record<string, string>;
|
||||
@@ -86,7 +90,7 @@ type Source = {
|
||||
index: string;
|
||||
scope?: 'local' | 'global' | string[];
|
||||
};
|
||||
sentryForBackend?: SentryBackendConfig;
|
||||
sentryForBackend?: SentryBackendConfigSource;
|
||||
otelForBackend?: OtelBackendConfig;
|
||||
sentryForFrontend?: {
|
||||
options: Partial<SentryVue.BrowserOptions> & { 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);
|
||||
|
||||
@@ -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<ClientRequest, HttpClientSpan>();
|
||||
|
||||
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<HttpClientInstrumentationDeps, 'subscribe'>): () => 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<HttpClientInstrument
|
||||
}
|
||||
|
||||
function getRequestDetails(request: ClientRequest): { url: string; host: string; port: number } {
|
||||
const protocol = request.protocol ?? 'http:';
|
||||
const host = request.getHeader('host')?.toString() ?? request.host ?? 'localhost';
|
||||
const url = new URL(request.path || '/', `${protocol}//${host}`);
|
||||
// URL 属性には認証情報やクエリ文字列を含めない。
|
||||
url.username = '';
|
||||
url.password = '';
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
let protocol: 'http:' | 'https:' = 'http:';
|
||||
|
||||
return {
|
||||
url: url.toString(),
|
||||
host: url.hostname,
|
||||
// URL.port は既定ポートでは空文字列になるため、スキームから補う。
|
||||
port: url.port === '' ? (url.protocol === 'https:' ? 443 : 80) : Number(url.port),
|
||||
try {
|
||||
protocol = request.protocol === 'https:' ? 'https:' : 'http:';
|
||||
const host = request.getHeader('host')?.toString() ?? request.host;
|
||||
const url = new URL(request.path || '/', `${protocol}//${host}`);
|
||||
// URL 属性には認証情報やクエリ文字列を含めない。
|
||||
url.username = '';
|
||||
url.password = '';
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
|
||||
return {
|
||||
url: url.toString(),
|
||||
host: url.hostname,
|
||||
// URL.port は既定ポートでは空文字列になるため、スキームから補う。
|
||||
port: url.port === '' ? (url.protocol === 'https:' ? 443 : 80) : Number(url.port),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
url: `${protocol}//localhost/`,
|
||||
host: 'localhost',
|
||||
port: protocol === 'https:' ? 443 : 80,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function guardDiagnosticsListener(
|
||||
deps: Pick<HttpClientInstrumentationDeps, 'reportError'>,
|
||||
listener: (message: unknown) => void,
|
||||
): (message: unknown) => void {
|
||||
return (message) => {
|
||||
try {
|
||||
listener(message);
|
||||
} catch (error) {
|
||||
try {
|
||||
deps.reportError?.(error);
|
||||
} catch {
|
||||
// diagnostics listener の失敗をアプリケーションへ伝播させない。
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T extends object>(
|
||||
};
|
||||
|
||||
const subscribers: TracingChannelSubscribers<T> = {
|
||||
start: (message) => {
|
||||
start: guardTracingSubscriber(deps, (message: T) => {
|
||||
if (requireParentSpan && deps.getActiveSpan() == null) return;
|
||||
|
||||
const options = getSpanOptions(message);
|
||||
@@ -147,24 +149,41 @@ function createTracingChannelSubscribers<T extends object>(
|
||||
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<T>(
|
||||
deps: Pick<RedisInstrumentationDeps, 'reportError'>,
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -73,7 +73,10 @@ export function startSpanWithTraceContext<T>(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<void> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<ReturnType<typeof buildSentryIntegrations>>[0][number];
|
||||
type TestSpanProcessor = NonNullable<ReturnType<typeof buildSentryOtlpInitOptions>['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/']);
|
||||
});
|
||||
|
||||
@@ -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<string, (message: unknown) => 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<string, (message: unknown) => 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<string, any>();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
Generated
+2
-2
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user