mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-28 18:14:37 +02:00
feat: OTelのHTTPサーバ自動計装を追加 (#17711)
This commit is contained in:
@@ -57,6 +57,7 @@
|
||||
"@fastify/cors": "11.2.0",
|
||||
"@fastify/http-proxy": "11.5.0",
|
||||
"@fastify/multipart": "10.0.0",
|
||||
"@fastify/otel": "0.19.0",
|
||||
"@fastify/static": "9.1.3",
|
||||
"@kitajs/html": "4.2.13",
|
||||
"@misskey-dev/emoji-assets": "17.0.3",
|
||||
|
||||
@@ -32,6 +32,7 @@ import { HealthServerService } from './HealthServerService.js';
|
||||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
import { registerHttpServerInstrumentation } from './http-server-instrumentation.js';
|
||||
|
||||
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
@@ -80,6 +81,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||
logger: false,
|
||||
});
|
||||
this.#fastify = fastify;
|
||||
await registerHttpServerInstrumentation(fastify, this.config);
|
||||
|
||||
// HSTS
|
||||
// 6months (15552000sec)
|
||||
|
||||
@@ -151,7 +151,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
endpoint: IEndpoint & { exec: any },
|
||||
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||
reply: FastifyReply,
|
||||
): void {
|
||||
): Promise<void> {
|
||||
const body = request.method === 'GET'
|
||||
? request.query
|
||||
: request.body;
|
||||
@@ -162,10 +162,11 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
: body?.['i'];
|
||||
if (token != null && typeof token !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.authenticateService.authenticate(token).then(([user, app]) => {
|
||||
this.call(endpoint, user, app, body, null, request).then((res) => {
|
||||
|
||||
return this.telemetryService.startSpan('API: ' + endpoint.name, () => this.authenticateService.authenticate(token).then(([user, app]) => {
|
||||
const call = this.call(endpoint, user, app, body, null, request).then((res) => {
|
||||
if (request.method === 'GET' && endpoint.meta.cacheSec && !token && !user) {
|
||||
reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
|
||||
}
|
||||
@@ -177,9 +178,11 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
if (user) {
|
||||
this.logIp(request, user);
|
||||
}
|
||||
|
||||
return call;
|
||||
}).catch(err => {
|
||||
this.#sendAuthenticationError(reply, err);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -222,8 +225,9 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
this.authenticateService.authenticate(token).then(([user, app]) => {
|
||||
this.call(endpoint, user, app, fields, {
|
||||
|
||||
return await this.telemetryService.startSpan('API: ' + endpoint.name, () => this.authenticateService.authenticate(token).then(([user, app]) => {
|
||||
const call = this.call(endpoint, user, app, fields, {
|
||||
name: multipartData.filename,
|
||||
path: path,
|
||||
}, request).then((res) => {
|
||||
@@ -235,9 +239,11 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
if (user) {
|
||||
this.logIp(request, user);
|
||||
}
|
||||
|
||||
return call;
|
||||
}).catch(err => {
|
||||
this.#sendAuthenticationError(reply, err);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -431,9 +437,10 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
// API invoking
|
||||
return await this.telemetryService.startSpan('API: ' + ep.name, () => ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
|
||||
// The API span starts in handleRequest/handleMultipartRequest so it also covers
|
||||
// authentication, rate limiting, and parameter validation.
|
||||
return await ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
||||
35
packages/backend/src/server/http-server-instrumentation.ts
Normal file
35
packages/backend/src/server/http-server-instrumentation.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Config } from '@/config.js';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
type TelemetryConfig = Pick<Config, 'otelForBackend' | 'sentryForBackend'>;
|
||||
|
||||
export function shouldRegisterHttpServerInstrumentation(config: TelemetryConfig): boolean {
|
||||
// Sentryもリクエストspanを作成するため、両方を登録すると重複して出力される。
|
||||
return config.otelForBackend != null && config.sentryForBackend == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* すべてのルート・フックより前にリクエスト計装を登録し、ActivityPubや
|
||||
* well-knownを含む全HTTP受信経路を1つのroot spanとして計測する。
|
||||
*/
|
||||
export async function registerHttpServerInstrumentation(fastify: FastifyInstance, config: TelemetryConfig): Promise<void> {
|
||||
if (!shouldRegisterHttpServerInstrumentation(config)) return;
|
||||
|
||||
const { FastifyOtelInstrumentation } = await import('@fastify/otel');
|
||||
const instrumentation = new FastifyOtelInstrumentation({
|
||||
requestHook: (span, request) => {
|
||||
const route = request.routeOptions.url;
|
||||
if (route != null) {
|
||||
// デフォルトだとトレース名が「request」で固定されてしまうため、判別がつかなくなる。
|
||||
// ルート名をspan名に設定することで、トレースビューでルートごとの処理時間を確認できるようになる。
|
||||
span.updateName(`${request.method} ${route}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
await fastify.register(instrumentation.plugin());
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { shouldRegisterHttpServerInstrumentation, registerHttpServerInstrumentation } from '@/server/http-server-instrumentation.js';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
plugin: vi.fn(),
|
||||
instrumentation: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@fastify/otel', () => ({
|
||||
FastifyOtelInstrumentation: class {
|
||||
public plugin = mocks.plugin;
|
||||
|
||||
public constructor(options: unknown) {
|
||||
mocks.instrumentation(options);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('http-server-instrumentation', () => {
|
||||
test('registers Fastify instrumentation when only OpenTelemetry is configured', async () => {
|
||||
const plugin = vi.fn();
|
||||
const fastify = { register: vi.fn().mockResolvedValue(undefined) };
|
||||
mocks.plugin.mockReturnValue(plugin);
|
||||
|
||||
await registerHttpServerInstrumentation(fastify as any, { otelForBackend: {} } as any);
|
||||
|
||||
expect(mocks.instrumentation).toHaveBeenCalledTimes(1);
|
||||
expect(fastify.register).toHaveBeenCalledWith(plugin);
|
||||
|
||||
const requestHook = mocks.instrumentation.mock.calls[0][0].requestHook;
|
||||
const span = { updateName: vi.fn() };
|
||||
requestHook(span, { method: 'POST', routeOptions: { url: '/notes/create' } });
|
||||
expect(span.updateName).toHaveBeenCalledWith('POST /notes/create');
|
||||
});
|
||||
|
||||
test('does not register duplicate request instrumentation with Sentry', async () => {
|
||||
const fastify = { register: vi.fn() };
|
||||
|
||||
await registerHttpServerInstrumentation(fastify as any, { otelForBackend: {}, sentryForBackend: {} } as any);
|
||||
|
||||
expect(fastify.register).not.toHaveBeenCalled();
|
||||
expect(shouldRegisterHttpServerInstrumentation({ otelForBackend: {}, sentryForBackend: {} } as any)).toBe(false);
|
||||
});
|
||||
|
||||
test('does not register instrumentation without OpenTelemetry', () => {
|
||||
expect(shouldRegisterHttpServerInstrumentation({} as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user