refactor(server): standarderlize metrics and trace with OTEL (#5054)

you can now export span to Zipkin and metrics to Prometheus when developing locally
follow the docs of OTEL: https://opentelemetry.io/docs/instrumentation/js/exporters/

<img width="2357" alt="image" src="https://github.com/toeverything/AFFiNE/assets/8281226/ec615e1f-3e91-43f7-9111-d7d2629e9679">
This commit is contained in:
liuyi
2023-11-24 15:19:22 +00:00
parent cf65a5cd93
commit 91efca107a
23 changed files with 298 additions and 360 deletions

View File

@@ -23,7 +23,7 @@ import type { AuthAction, CookieOption, NextAuthOptions } from 'next-auth';
import { AuthHandler } from 'next-auth/core';
import { Config } from '../../config';
import { Metrics } from '../../metrics/metrics';
import { metrics } from '../../metrics';
import { PrismaService } from '../../prisma/service';
import { SessionService } from '../../session';
import { AuthThrottlerGuard, Throttle } from '../../throttler';
@@ -46,7 +46,6 @@ export class NextAuthController {
private readonly authService: AuthService,
@Inject(NextAuthOptionsProvide)
private readonly nextAuthOptions: NextAuthOptions,
private readonly metrics: Metrics,
private readonly session: SessionService
) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -90,7 +89,7 @@ export class NextAuthController {
res.redirect(`/signin${query}`);
return;
}
this.metrics.authCounter(1, {});
metrics().authCounter.add(1);
const [action, providerId] = req.url // start with request url
.slice(BASE_URL.length) // make relative to baseUrl
.replace(/\?.*/, '') // remove query part, use only path part
@@ -127,7 +126,7 @@ export class NextAuthController {
const options = this.nextAuthOptions;
if (req.method === 'POST' && action === 'session') {
if (typeof req.body !== 'object' || typeof req.body.data !== 'object') {
this.metrics.authFailCounter(1, { reason: 'invalid_session_data' });
metrics().authFailCounter.add(1, { reason: 'invalid_session_data' });
throw new BadRequestException(`Invalid new session data`);
}
const user = await this.updateSession(req, req.body.data);
@@ -210,7 +209,7 @@ export class NextAuthController {
if (redirect?.endsWith('api/auth/error?error=AccessDenied')) {
this.logger.log(`Early access redirect headers: ${req.headers}`);
this.metrics.authFailCounter(1, {
metrics().authFailCounter.add(1, {
reason: 'no_early_access_permission',
});
if (

View File

@@ -6,7 +6,7 @@ import { Cron, CronExpression } from '@nestjs/schedule';
import type { Snapshot } from '@prisma/client';
import { Config } from '../../config';
import { Metrics } from '../../metrics';
import { metrics } from '../../metrics';
import { PrismaService } from '../../prisma';
import { SubscriptionStatus } from '../payment/service';
import { Permission } from '../workspaces/types';
@@ -16,8 +16,7 @@ export class DocHistoryManager {
private readonly logger = new Logger(DocHistoryManager.name);
constructor(
private readonly config: Config,
private readonly db: PrismaService,
private readonly metrics: Metrics
private readonly db: PrismaService
) {}
@OnEvent('doc:manager:snapshot:beforeUpdate')
@@ -69,7 +68,7 @@ export class DocHistoryManager {
// safe to ignore
// only happens when duplicated history record created in multi processes
});
this.metrics.docHistoryCounter(1, {});
metrics().docHistoryCounter.add(1, {});
this.logger.log(
`History created for ${snapshot.id} in workspace ${snapshot.workspaceId}.`
);
@@ -183,7 +182,7 @@ export class DocHistoryManager {
// which is not the solution in CRDT.
// let user revert in client and update the data in sync system
// `await this.db.snapshot.update();`
this.metrics.docRecoverCounter(1, {});
metrics().docRecoverCounter.add(1, {});
return history.timestamp;
}

View File

@@ -19,7 +19,7 @@ import {
import { Cache } from '../../cache';
import { Config } from '../../config';
import { Metrics } from '../../metrics/metrics';
import { metrics } from '../../metrics/metrics';
import { PrismaService } from '../../prisma';
import { mergeUpdatesInApplyWay as jwstMergeUpdates } from '../../storage';
@@ -70,7 +70,6 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
private readonly automation: boolean,
private readonly db: PrismaService,
private readonly config: Config,
private readonly metrics: Metrics,
private readonly cache: Cache,
private readonly event: EventEmitter2
) {}
@@ -126,13 +125,13 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
this.config.doc.manager.experimentalMergeWithJwstCodec &&
updates.length < 100 /* avoid overloading */
) {
this.metrics.jwstCodecMerge(1, {});
metrics().jwstCodecMerge.add(1);
const yjsResult = Buffer.from(encodeStateAsUpdate(doc));
let log = false;
try {
const jwstResult = jwstMergeUpdates(updates);
if (!compare(yjsResult, jwstResult)) {
this.metrics.jwstCodecDidnotMatch(1, {});
metrics().jwstCodecDidnotMatch.add(1);
this.logger.warn(
`jwst codec result doesn't match yjs codec result for: ${guid}`
);
@@ -143,7 +142,7 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
}
}
} catch (e) {
this.metrics.jwstCodecFail(1, {});
metrics().jwstCodecFail.add(1);
this.logger.warn(`jwst apply update failed for ${guid}: ${e}`);
log = true;
} finally {

View File

@@ -11,8 +11,8 @@ import {
import { Server, Socket } from 'socket.io';
import { encodeStateAsUpdate, encodeStateVector } from 'yjs';
import { Metrics } from '../../../metrics/metrics';
import { CallCounter, CallTimer } from '../../../metrics/utils';
import { metrics } from '../../../metrics';
import { CallTimer } from '../../../metrics/utils';
import { DocID } from '../../../utils/doc';
import { Auth, CurrentUser } from '../../auth';
import { DocManager } from '../../doc';
@@ -68,8 +68,7 @@ export const GatewayErrorWrapper = (): MethodDecorator => {
const SubscribeMessage = (event: string) =>
applyDecorators(
GatewayErrorWrapper(),
CallCounter('socket_io_counter', { event }),
CallTimer('socket_io_timer', { event }),
CallTimer('socket_io_event_duration', { event }),
RawSubscribeMessage(event)
);
@@ -97,7 +96,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
constructor(
private readonly docManager: DocManager,
private readonly metric: Metrics,
private readonly permissions: PermissionService
) {}
@@ -106,12 +104,12 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
handleConnection() {
this.connectionCount++;
this.metric.socketIOConnectionGauge(this.connectionCount, {});
metrics().socketIOConnectionGauge(this.connectionCount);
}
handleDisconnect() {
this.connectionCount--;
this.metric.socketIOConnectionGauge(this.connectionCount, {});
metrics().socketIOConnectionGauge(this.connectionCount);
}
@Auth()

View File

@@ -4,14 +4,13 @@ import {
ForbiddenException,
Get,
Inject,
Logger,
NotFoundException,
Param,
Res,
} from '@nestjs/common';
import type { Response } from 'express';
import format from 'pretty-time';
import { CallTimer } from '../../metrics';
import { PrismaService } from '../../prisma';
import { StorageProvide } from '../../storage';
import { DocID } from '../../utils/doc';
@@ -23,8 +22,6 @@ import { Permission } from './types';
@Controller('/api/workspaces')
export class WorkspacesController {
private readonly logger = new Logger('WorkspacesController');
constructor(
@Inject(StorageProvide) private readonly storage: Storage,
private readonly permission: PermissionService,
@@ -37,6 +34,7 @@ export class WorkspacesController {
//
// NOTE: because graphql can't represent a File, so we have to use REST API to get blob
@Get('/:id/blobs/:name')
@CallTimer('doc_controller', { method: 'get_blob' })
async blob(
@Param('id') workspaceId: string,
@Param('name') name: string,
@@ -61,13 +59,13 @@ export class WorkspacesController {
@Get('/:id/docs/:guid')
@Auth()
@Publicable()
@CallTimer('doc_controller', { method: 'get_doc' })
async doc(
@CurrentUser() user: UserType | undefined,
@Param('id') ws: string,
@Param('guid') guid: string,
@Res() res: Response
) {
const start = process.hrtime();
const docId = new DocID(guid, ws);
if (
// if a user has the permission
@@ -104,11 +102,11 @@ export class WorkspacesController {
res.setHeader('content-type', 'application/octet-stream');
res.send(update);
this.logger.debug(`workspaces doc api: ${format(process.hrtime(start))}`);
}
@Get('/:id/docs/:guid/histories/:timestamp')
@Auth()
@CallTimer('doc_controller', { method: 'get_history' })
async history(
@CurrentUser() user: UserType,
@Param('id') ws: string,