mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-27 08:24:35 +02:00
181 lines
6.7 KiB
TypeScript
181 lines
6.7 KiB
TypeScript
/*
|
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
import type { Config } from '@/config.js';
|
|
|
|
const mocks = vi.hoisted(() => {
|
|
return {
|
|
sentryCreate: vi.fn(),
|
|
sentryCreateWithOtlpExport: vi.fn(),
|
|
otelCreate: vi.fn(),
|
|
setLogTraceContextProvider: vi.fn(),
|
|
};
|
|
});
|
|
|
|
vi.mock('@/logging/logging-runtime.js', () => ({
|
|
setLogTraceContextProvider: mocks.setLogTraceContextProvider,
|
|
}));
|
|
|
|
vi.mock('@/core/telemetry/adapters/SentryTelemetryAdapter.js', () => ({
|
|
SentryTelemetryAdapter: {
|
|
create: mocks.sentryCreate,
|
|
createWithOtlpExport: mocks.sentryCreateWithOtlpExport,
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/core/telemetry/adapters/OpenTelemetryAdapter.js', () => ({
|
|
OpenTelemetryAdapter: {
|
|
create: mocks.otelCreate,
|
|
},
|
|
}));
|
|
|
|
function config(overrides: Partial<Config>): Config {
|
|
return {
|
|
version: '2026.1.0',
|
|
...overrides,
|
|
} as Config;
|
|
}
|
|
|
|
describe('telemetry-registry', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
mocks.sentryCreate.mockReset();
|
|
mocks.sentryCreateWithOtlpExport.mockReset();
|
|
mocks.otelCreate.mockReset();
|
|
mocks.setLogTraceContextProvider.mockReset();
|
|
mocks.sentryCreate.mockResolvedValue({ shutdown: vi.fn(), captureMessage: vi.fn(), startSpan: vi.fn() });
|
|
mocks.sentryCreateWithOtlpExport.mockResolvedValue({ shutdown: vi.fn(), captureMessage: vi.fn(), startSpan: vi.fn() });
|
|
mocks.otelCreate.mockResolvedValue({ shutdown: vi.fn(), captureMessage: vi.fn(), startSpan: vi.fn() });
|
|
});
|
|
|
|
test('uses OpenTelemetryAdapter when only otelForBackend is configured', async () => {
|
|
const { initTelemetry } = await import('@/core/telemetry/telemetry-registry.js');
|
|
const otelForBackend = { endpoint: 'http://collector:4318/v1/traces' };
|
|
|
|
await initTelemetry(config({ otelForBackend }));
|
|
|
|
expect(mocks.otelCreate).toHaveBeenCalledWith({
|
|
...otelForBackend,
|
|
serviceVersion: '2026.1.0',
|
|
});
|
|
expect(mocks.sentryCreate).not.toHaveBeenCalled();
|
|
expect(mocks.sentryCreateWithOtlpExport).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('registers the adapter trace context provider after telemetry initialization', async () => {
|
|
const { initTelemetry } = await import('@/core/telemetry/telemetry-registry.js');
|
|
const getActiveTraceContext = vi.fn(() => ({
|
|
traceId: '0123456789abcdef0123456789abcdef',
|
|
spanId: '0123456789abcdef',
|
|
traceFlags: 0,
|
|
}));
|
|
mocks.otelCreate.mockResolvedValue({
|
|
shutdown: vi.fn(),
|
|
captureMessage: vi.fn(),
|
|
startSpan: vi.fn(),
|
|
getActiveTraceContext,
|
|
});
|
|
|
|
await initTelemetry(config({ otelForBackend: { endpoint: 'http://collector:4318/v1/traces' } }));
|
|
|
|
expect(mocks.setLogTraceContextProvider).toHaveBeenCalledWith(expect.any(Function));
|
|
const provider = mocks.setLogTraceContextProvider.mock.calls[0][0] as () => unknown;
|
|
expect(provider()).toEqual({
|
|
traceId: '0123456789abcdef0123456789abcdef',
|
|
spanId: '0123456789abcdef',
|
|
traceFlags: 0,
|
|
});
|
|
expect(getActiveTraceContext).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
test('adds OTLP export to the Sentry provider when both Sentry and OTel are configured', async () => {
|
|
const { initTelemetry } = await import('@/core/telemetry/telemetry-registry.js');
|
|
const sentryForBackend = { options: {}, enableNodeProfiling: false };
|
|
const otelForBackend = { endpoint: 'http://collector:4318/v1/traces' };
|
|
|
|
await initTelemetry(config({ sentryForBackend, otelForBackend }));
|
|
|
|
expect(mocks.sentryCreateWithOtlpExport).toHaveBeenCalledWith(sentryForBackend, {
|
|
...otelForBackend,
|
|
serviceVersion: '2026.1.0',
|
|
});
|
|
expect(mocks.sentryCreate).not.toHaveBeenCalled();
|
|
expect(mocks.otelCreate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('startSpan runs fn directly when no adapter is registered', async () => {
|
|
const { startSpan } = await import('@/core/telemetry/telemetry-registry.js');
|
|
|
|
const fn = vi.fn().mockReturnValue('result');
|
|
expect(startSpan('test', fn)).toBe('result');
|
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('startSpan delegates directly to the single registered adapter without extra wrapping', async () => {
|
|
const { initTelemetry, startSpan } = await import('@/core/telemetry/telemetry-registry.js');
|
|
const otelForBackend = { endpoint: 'http://collector:4318/v1/traces' };
|
|
const adapterStartSpan = vi.fn((_name: string, fn: () => string) => fn());
|
|
mocks.otelCreate.mockResolvedValue({ shutdown: vi.fn(), captureMessage: vi.fn(), startSpan: adapterStartSpan });
|
|
|
|
await initTelemetry(config({ otelForBackend }));
|
|
|
|
const fn = vi.fn().mockReturnValue('result');
|
|
expect(startSpan('test', fn)).toBe('result');
|
|
expect(adapterStartSpan).toHaveBeenCalledWith('test', fn);
|
|
});
|
|
|
|
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[] = [];
|
|
mocks.sentryCreate.mockResolvedValue({
|
|
shutdown: vi.fn(),
|
|
captureMessage: vi.fn(),
|
|
startSpan: vi.fn((_name: string, fn: () => string) => {
|
|
calls.push('sentry:start');
|
|
const result = fn();
|
|
calls.push('sentry:end');
|
|
return result;
|
|
}),
|
|
});
|
|
mocks.otelCreate.mockResolvedValue({
|
|
shutdown: vi.fn(),
|
|
captureMessage: vi.fn(),
|
|
startSpan: vi.fn((_name: string, fn: () => string) => {
|
|
calls.push('otel:start');
|
|
const result = fn();
|
|
calls.push('otel:end');
|
|
return result;
|
|
}),
|
|
});
|
|
|
|
await initTelemetry(config({ sentryForBackend: { options: {}, enableNodeProfiling: false } }));
|
|
await initTelemetry(config({ otelForBackend: { endpoint: 'http://collector:4318/v1/traces' } }));
|
|
|
|
const fn = vi.fn(() => {
|
|
calls.push('work');
|
|
return 'result';
|
|
});
|
|
|
|
expect(startSpan('test', fn)).toBe('result');
|
|
expect(calls).toEqual(['sentry:start', 'otel:start', 'work', 'otel:end', 'sentry:end']);
|
|
});
|
|
|
|
test('shutdownTelemetry waits for every adapter even when one shutdown rejects', async () => {
|
|
const { initTelemetry, shutdownTelemetry } = await import('@/core/telemetry/telemetry-registry.js');
|
|
const sentryShutdown = vi.fn().mockRejectedValue(new Error('sentry failed'));
|
|
const otelShutdown = vi.fn().mockResolvedValue(undefined);
|
|
mocks.sentryCreate.mockResolvedValue({ shutdown: sentryShutdown, captureMessage: vi.fn(), startSpan: vi.fn() });
|
|
mocks.otelCreate.mockResolvedValue({ shutdown: otelShutdown, captureMessage: vi.fn(), startSpan: vi.fn() });
|
|
|
|
await initTelemetry(config({ sentryForBackend: { options: {}, enableNodeProfiling: false } }));
|
|
await initTelemetry(config({ otelForBackend: { endpoint: 'http://collector:4318/v1/traces' } }));
|
|
|
|
await expect(shutdownTelemetry()).resolves.toBeUndefined();
|
|
expect(sentryShutdown).toHaveBeenCalledTimes(1);
|
|
expect(otelShutdown).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|