mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-31 12:25:49 +00:00
feat: BullMQのジョブ追加時に発動する手動計測の追加 (#17710)
* feat: BullMQのジョブ追加時に発動する手動計測の追加 * add comment
This commit is contained in:
@@ -8,6 +8,7 @@ import { SpanStatusCode } from '@opentelemetry/api';
|
||||
import { defaultResource, detectResources, envDetector, resourceFromAttributes } from '@opentelemetry/resources';
|
||||
import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';
|
||||
import { ATTR_SERVICE_INSTANCE_ID, ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
||||
import type { Context, SpanContext } from '@opentelemetry/api';
|
||||
import { OpenTelemetryAdapter, createResource, createSampler, getMisskeyProcessRole } from '@/core/telemetry/adapters/OpenTelemetryAdapter.js';
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
@@ -97,6 +98,61 @@ describe('OpenTelemetryAdapter', () => {
|
||||
expect(span.end).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('creates a root worker span linked to the enqueue span by default', () => {
|
||||
const span = {
|
||||
end: vi.fn(),
|
||||
recordException: vi.fn(),
|
||||
setStatus: vi.fn(),
|
||||
};
|
||||
const rootContext = {} as Context;
|
||||
const extractedContext = {} as Context;
|
||||
const sourceSpanContext = {
|
||||
traceId: '0123456789abcdef0123456789abcdef',
|
||||
spanId: '0123456789abcdef',
|
||||
traceFlags: 1,
|
||||
isRemote: true,
|
||||
} as SpanContext;
|
||||
const propagation = {
|
||||
isPropagationApi: true,
|
||||
inject: vi.fn(),
|
||||
extract(this: { isPropagationApi: boolean }) {
|
||||
if (!this.isPropagationApi) throw new Error('lost propagation API receiver');
|
||||
return extractedContext;
|
||||
},
|
||||
};
|
||||
const tracer = {
|
||||
startActiveSpan: vi.fn((_name: string, _options: unknown, _context: unknown, fn: (spanArg: typeof span) => string) => fn(span)),
|
||||
} as any;
|
||||
const adapter = new OpenTelemetryAdapter({
|
||||
tracer,
|
||||
provider: { shutdown: vi.fn() },
|
||||
getActiveSpan: () => undefined,
|
||||
spanStatusCodeError: SpanStatusCode.ERROR,
|
||||
shutdownTimeout: 10,
|
||||
queueTraceContext: {
|
||||
tracer,
|
||||
propagation: propagation as any,
|
||||
trace: { getSpanContext: () => sourceSpanContext },
|
||||
getActiveContext: () => rootContext,
|
||||
rootContext,
|
||||
mode: 'link',
|
||||
spanStatusCodeError: SpanStatusCode.ERROR,
|
||||
},
|
||||
});
|
||||
|
||||
expect(adapter.startSpanWithTraceContext('Queue: Deliver', {
|
||||
__misskeyTraceContext: {
|
||||
traceparent: '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01',
|
||||
},
|
||||
}, () => 'ok')).toBe('ok');
|
||||
|
||||
expect(tracer.startActiveSpan).toHaveBeenCalledWith('Queue: Deliver', {
|
||||
root: true,
|
||||
links: [{ context: sourceSpanContext }],
|
||||
}, rootContext, expect.any(Function));
|
||||
expect(span.end).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('bridges captureMessage to the active span when one exists', () => {
|
||||
const activeSpan = {
|
||||
recordException: vi.fn(),
|
||||
|
||||
@@ -208,8 +208,13 @@ describe('SentryTelemetryAdapter.createWithOtlpExport', () => {
|
||||
nodeProfilingIntegration,
|
||||
}));
|
||||
vi.doMock('@opentelemetry/api', () => ({
|
||||
context: { active: vi.fn() },
|
||||
diag: { setLogger },
|
||||
DiagLogLevel: { WARN: 50 },
|
||||
propagation: { inject: vi.fn(), extract: vi.fn() },
|
||||
ROOT_CONTEXT: {},
|
||||
SpanStatusCode: { ERROR: 2 },
|
||||
trace: { getTracer: vi.fn(), getSpanContext: vi.fn() },
|
||||
}));
|
||||
vi.doMock('@opentelemetry/sdk-trace-base', () => ({
|
||||
BatchSpanProcessor,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import type * as Bull from 'bullmq';
|
||||
import { instrumentQueue } from '@/core/telemetry/queue-instrumentation.js';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
injectTraceContext: vi.fn((carrier: Record<string, string>) => {
|
||||
carrier['traceparent'] = '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01';
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/core/telemetry/telemetry-registry.js', () => ({
|
||||
injectTraceContext: mocks.injectTraceContext,
|
||||
}));
|
||||
|
||||
describe('queue-instrumentation', () => {
|
||||
beforeEach(() => {
|
||||
mocks.injectTraceContext.mockClear();
|
||||
});
|
||||
|
||||
test('injects the active trace context for add()', () => {
|
||||
const add = vi.fn();
|
||||
const queue = instrumentQueue({ add, addBulk: vi.fn() } as unknown as Bull.Queue<{ noteId: string }>);
|
||||
const data = { noteId: '9d6b9a65-46c9-4e1b-a640-9589693893c9' };
|
||||
|
||||
queue.add('endedPollNotification', data);
|
||||
|
||||
expect(mocks.injectTraceContext).toHaveBeenCalledTimes(1);
|
||||
expect(data).toMatchObject({
|
||||
__misskeyTraceContext: {
|
||||
traceparent: '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01',
|
||||
},
|
||||
});
|
||||
expect(add).toHaveBeenCalledWith('endedPollNotification', data, undefined);
|
||||
});
|
||||
|
||||
test('injects every job passed to addBulk()', () => {
|
||||
const addBulk = vi.fn();
|
||||
const queue = instrumentQueue({ add: vi.fn(), addBulk } as unknown as Bull.Queue<{ to: string }>);
|
||||
const jobs = [
|
||||
{ name: 'deliver', data: { to: 'https://remote.example/inbox' } },
|
||||
{ name: 'deliver', data: { to: 'https://remote2.example/inbox' } },
|
||||
];
|
||||
|
||||
queue.addBulk(jobs);
|
||||
|
||||
expect(mocks.injectTraceContext).toHaveBeenCalledTimes(2);
|
||||
expect(jobs).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ data: expect.objectContaining({ __misskeyTraceContext: expect.any(Object) }) }),
|
||||
]));
|
||||
expect(addBulk).toHaveBeenCalledWith(jobs);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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 sourceSpanContext: SpanContext = {
|
||||
traceId: '0123456789abcdef0123456789abcdef',
|
||||
spanId: '0123456789abcdef',
|
||||
traceFlags: 1,
|
||||
isRemote: true,
|
||||
};
|
||||
|
||||
function jobData() {
|
||||
return {
|
||||
name: 'deliver',
|
||||
__misskeyTraceContext: {
|
||||
traceparent: '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('queue-trace-context', () => {
|
||||
test('stores only a non-empty carrier in the job data', () => {
|
||||
const data = { noteId: '9d6b9a65-46c9-4e1b-a640-9589693893c9' };
|
||||
|
||||
injectQueueTraceContext(data, carrier => {
|
||||
carrier['traceparent'] = '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01';
|
||||
});
|
||||
|
||||
expect(data).toMatchObject({
|
||||
__misskeyTraceContext: {
|
||||
traceparent: '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('does not store an empty carrier when no active trace exists', () => {
|
||||
const data = { noteId: '9d6b9a65-46c9-4e1b-a640-9589693893c9' };
|
||||
|
||||
injectQueueTraceContext(data, () => {});
|
||||
|
||||
expect(data).not.toHaveProperty('__misskeyTraceContext');
|
||||
});
|
||||
|
||||
test('ignores non-object job data', () => {
|
||||
const inject = vi.fn();
|
||||
|
||||
injectQueueTraceContext(null, inject);
|
||||
injectQueueTraceContext('not a job object', inject);
|
||||
|
||||
expect(inject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('injects the active context with the configured propagator', () => {
|
||||
const activeContext = {} as Context;
|
||||
const carrier = {};
|
||||
const inject = vi.fn();
|
||||
|
||||
injectActiveTraceContext({
|
||||
tracer: { startActiveSpan: vi.fn() } as any,
|
||||
propagation: { inject, extract: vi.fn() } as any,
|
||||
trace: { getSpanContext: vi.fn() },
|
||||
getActiveContext: () => activeContext,
|
||||
rootContext,
|
||||
mode: 'link',
|
||||
spanStatusCodeError: 2 as any,
|
||||
}, carrier);
|
||||
|
||||
expect(inject).toHaveBeenCalledWith(activeContext, carrier);
|
||||
});
|
||||
|
||||
test('starts a new root trace with a link by default', () => {
|
||||
const extract = vi.fn(() => extractedContext);
|
||||
const getSpanContext = vi.fn(() => sourceSpanContext);
|
||||
|
||||
const result = getQueueSpanContext(jobData(), {
|
||||
rootContext,
|
||||
propagation: { inject: vi.fn(), extract },
|
||||
trace: { getSpanContext },
|
||||
mode: 'link',
|
||||
});
|
||||
|
||||
expect(extract).toHaveBeenCalledWith(rootContext, jobData().__misskeyTraceContext);
|
||||
expect(result).toEqual({
|
||||
options: {
|
||||
root: true,
|
||||
links: [{ context: sourceSpanContext }],
|
||||
},
|
||||
parentContext: rootContext,
|
||||
});
|
||||
});
|
||||
|
||||
test('uses the extracted context as the parent when parent mode is selected', () => {
|
||||
const result = getQueueSpanContext(jobData(), {
|
||||
rootContext,
|
||||
propagation: { inject: vi.fn(), extract: () => extractedContext },
|
||||
trace: { getSpanContext: () => sourceSpanContext },
|
||||
mode: 'parent',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
options: {},
|
||||
parentContext: extractedContext,
|
||||
});
|
||||
});
|
||||
|
||||
test('ignores malformed or missing carriers', () => {
|
||||
const extract = vi.fn(() => extractedContext);
|
||||
const deps = {
|
||||
rootContext,
|
||||
propagation: { inject: vi.fn(), extract },
|
||||
trace: { getSpanContext: () => sourceSpanContext },
|
||||
mode: 'link' as const,
|
||||
};
|
||||
|
||||
expect(getQueueSpanContext({}, deps)).toBeUndefined();
|
||||
expect(getQueueSpanContext({ __misskeyTraceContext: { traceparent: 1 } }, deps)).toBeUndefined();
|
||||
expect(extract).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('defaults to link mode and rejects invalid configuration', () => {
|
||||
expect(getQueueTraceContextMode(undefined)).toBe('link');
|
||||
expect(getQueueTraceContextMode('parent')).toBe('parent');
|
||||
expect(() => getQueueTraceContextMode('children')).toThrow('otelForBackend.jobTraceContextMode');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user