mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 20:38:52 +00:00
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:
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user