diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 6ff2a9065da6..05a7803f7bf1 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -9,12 +9,12 @@ }, "scripts": { "build": "tsc", - "start": "node --loader ts-node/esm/transpile-only.mjs ./src/index.ts", + "start": "node --import ./scripts/register.js ./src/index.ts", "dev": "nodemon ./src/index.ts", "test": "ava --concurrency 1 --serial", "test:coverage": "c8 ava --concurrency 1 --serial", "postinstall": "prisma generate", - "data-migration": "node --loader ts-node/esm/transpile-only.mjs ./src/data/index.ts", + "data-migration": "node --import ./scripts/register.js ./src/data/index.ts", "predeploy": "yarn prisma migrate deploy && node --import ./scripts/register.js ./dist/data/index.js run" }, "dependencies": { @@ -40,8 +40,8 @@ "@node-rs/jsonwebtoken": "^0.5.2", "@opentelemetry/api": "^1.8.0", "@opentelemetry/core": "^1.24.1", - "@opentelemetry/exporter-prometheus": "^0.51.1", - "@opentelemetry/exporter-zipkin": "^1.24.1", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.51.1", + "@opentelemetry/exporter-trace-otlp-proto": "^0.51.1", "@opentelemetry/host-metrics": "^0.35.1", "@opentelemetry/instrumentation": "^0.51.1", "@opentelemetry/instrumentation-graphql": "^0.40.0", @@ -55,7 +55,7 @@ "@opentelemetry/sdk-trace-node": "^1.24.1", "@opentelemetry/semantic-conventions": "^1.24.1", "@prisma/client": "^5.12.1", - "@prisma/instrumentation": "^5.12.1", + "@prisma/instrumentation": "^5.14", "@socket.io/redis-adapter": "^8.3.0", "cookie-parser": "^1.4.6", "dotenv": "^16.4.5", @@ -153,9 +153,8 @@ "exec": "node", "script": "./src/index.ts", "nodeArgs": [ - "--loader", - "ts-node/esm.mjs", - "--es-module-specifier-resolution=node" + "--import", + "./scripts/register.js" ], "ignore": [ "**/__tests__/**", diff --git a/packages/backend/server/scripts/loader.js b/packages/backend/server/scripts/loader.js index ce31fb50966e..83fa58cdd497 100644 --- a/packages/backend/server/scripts/loader.js +++ b/packages/backend/server/scripts/loader.js @@ -1,11 +1,32 @@ -import { create, createEsmHooks } from 'ts-node'; +import * as otel from '@opentelemetry/instrumentation/hook.mjs'; +import { createEsmHooks, register } from 'ts-node'; -const service = create({ +const service = register({ experimentalSpecifierResolution: 'node', transpileOnly: true, logError: true, - skipProject: true, }); -const hooks = createEsmHooks(service); -export const resolve = hooks.resolve; +/** + * @type {import('ts-node').NodeLoaderHooksAPI2} + + */ +const ts = createEsmHooks(service); + +/** + * @type {import('ts-node').NodeLoaderHooksAPI2.ResolveHook} + */ +export const resolve = (specifier, context, defaultResolver) => { + return ts.resolve(specifier, context, (s, c) => { + return otel.resolve(s, c, defaultResolver); + }); +}; + +/** + * @type {import('ts-node').NodeLoaderHooksAPI2.LoadHook} + */ +export const load = async (url, context, defaultLoader) => { + return await otel.load(url, context, (u, c) => { + return ts.load(u, c, defaultLoader); + }); +}; diff --git a/packages/backend/server/src/data/migrations/1703756315970-unamed-account.ts b/packages/backend/server/src/data/migrations/1703756315970-unamed-account.ts index 59428ccd6e47..b2ccbcc35d88 100644 --- a/packages/backend/server/src/data/migrations/1703756315970-unamed-account.ts +++ b/packages/backend/server/src/data/migrations/1703756315970-unamed-account.ts @@ -9,9 +9,6 @@ export class UnamedAccount1703756315970 { const users = await db.$queryRaw< User[] >`SELECT * FROM users WHERE name ~ E'^[\\s\\u2000-\\u200F]*$';`; - console.log( - `renaming ${users.map(({ email }) => email).join('|')} users` - ); await Promise.all( users.map(({ id, email }) => diff --git a/packages/backend/server/src/fundamentals/metrics/instrumentations.ts b/packages/backend/server/src/fundamentals/metrics/instrumentations.ts new file mode 100644 index 000000000000..997331fdcba9 --- /dev/null +++ b/packages/backend/server/src/fundamentals/metrics/instrumentations.ts @@ -0,0 +1,32 @@ +import { Instrumentation } from '@opentelemetry/instrumentation'; +import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis'; +import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; +import { SocketIoInstrumentation } from '@opentelemetry/instrumentation-socket.io'; +import prismaInstrument from '@prisma/instrumentation'; + +const { PrismaInstrumentation } = prismaInstrument; + +let instrumentations: Instrumentation[] = []; + +export function registerInstrumentations(): void { + if (AFFiNE.metrics.enabled) { + instrumentations = [ + new NestInstrumentation(), + new IORedisInstrumentation(), + new SocketIoInstrumentation({ traceReserved: true }), + new GraphQLInstrumentation({ + mergeItems: true, + ignoreTrivialResolveSpans: true, + depth: 10, + }), + new HttpInstrumentation(), + new PrismaInstrumentation({ middleware: false }), + ]; + } +} + +export function getRegisteredInstrumentations(): Instrumentation[] { + return instrumentations; +} diff --git a/packages/backend/server/src/fundamentals/metrics/opentelemetry.ts b/packages/backend/server/src/fundamentals/metrics/opentelemetry.ts index 02e84b8c7894..ad3d9372f000 100644 --- a/packages/backend/server/src/fundamentals/metrics/opentelemetry.ts +++ b/packages/backend/server/src/fundamentals/metrics/opentelemetry.ts @@ -1,52 +1,82 @@ -import { OnModuleDestroy } from '@nestjs/common'; -import { metrics } from '@opentelemetry/api'; +import { Attributes, metrics } from '@opentelemetry/api'; import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator, } from '@opentelemetry/core'; -import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'; -import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; +import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'; import { HostMetrics } from '@opentelemetry/host-metrics'; import { Instrumentation } from '@opentelemetry/instrumentation'; -import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; -import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; -import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis'; -import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; -import { SocketIoInstrumentation } from '@opentelemetry/instrumentation-socket.io'; import { Resource } from '@opentelemetry/resources'; import type { MeterProvider } from '@opentelemetry/sdk-metrics'; -import { MetricProducer, MetricReader } from '@opentelemetry/sdk-metrics'; +import { + MetricProducer, + MetricReader, + PeriodicExportingMetricReader, +} from '@opentelemetry/sdk-metrics'; import { NodeSDK } from '@opentelemetry/sdk-node'; import { - BatchSpanProcessor, SpanExporter, TraceIdRatioBasedSampler, } from '@opentelemetry/sdk-trace-node'; import { + SEMRESATTRS_K8S_CLUSTER_NAME, SEMRESATTRS_K8S_NAMESPACE_NAME, - SEMRESATTRS_SERVICE_NAME, + SEMRESATTRS_K8S_POD_NAME, SEMRESATTRS_SERVICE_VERSION, } from '@opentelemetry/semantic-conventions'; -import prismaInstrument from '@prisma/instrumentation'; +import { getRegisteredInstrumentations } from './instrumentations'; import { PrismaMetricProducer } from './prisma'; -const { PrismaInstrumentation } = prismaInstrument; +function withBuiltinAttributesMetricReader( + reader: MetricReader, + attrs: Attributes +) { + const collect = reader.collect; + reader.collect = async options => { + const result = await collect.call(reader, options); + + result.resourceMetrics.scopeMetrics.forEach(metrics => { + metrics.metrics.forEach(metric => { + metric.dataPoints.forEach(dataPoint => { + // @ts-expect-error allow + dataPoint.attributes = Object.assign({}, attrs, dataPoint.attributes); + }); + }); + }); + + return result; + }; + + return reader; +} + +function withBuiltinAttributesSpanExporter( + exporter: SpanExporter, + attrs: Attributes +) { + const exportSpans = exporter.export; + exporter.export = (spans, callback) => { + spans.forEach(span => { + // patch span attributes + // @ts-expect-error allow + span.attributes = Object.assign({}, attrs, span.attributes); + }); + + return exportSpans.call(exporter, spans, callback); + }; + + return exporter; +} export abstract class OpentelemetryFactory { abstract getMetricReader(): MetricReader; abstract getSpanExporter(): SpanExporter; getInstractions(): Instrumentation[] { - return [ - new NestInstrumentation(), - new IORedisInstrumentation(), - new SocketIoInstrumentation({ traceReserved: true }), - new GraphQLInstrumentation({ mergeItems: true }), - new HttpInstrumentation(), - new PrismaInstrumentation(), - ]; + return getRegisteredInstrumentations(); } getMetricsProducers(): MetricProducer[] { @@ -55,20 +85,32 @@ export abstract class OpentelemetryFactory { getResource() { return new Resource({ + [SEMRESATTRS_K8S_CLUSTER_NAME]: AFFiNE.flavor.type, [SEMRESATTRS_K8S_NAMESPACE_NAME]: AFFiNE.AFFINE_ENV, - [SEMRESATTRS_SERVICE_NAME]: AFFiNE.flavor.type, - [SEMRESATTRS_SERVICE_VERSION]: AFFiNE.version, + [SEMRESATTRS_K8S_POD_NAME]: process.env.HOSTNAME ?? process.env.HOST, }); } + getBuiltinAttributes(): Attributes { + return { + [SEMRESATTRS_SERVICE_VERSION]: AFFiNE.version, + }; + } + create() { - const traceExporter = this.getSpanExporter(); + const builtinAttributes = this.getBuiltinAttributes(); + return new NodeSDK({ resource: this.getResource(), sampler: new TraceIdRatioBasedSampler(0.1), - traceExporter, - metricReader: this.getMetricReader(), - spanProcessor: new BatchSpanProcessor(traceExporter), + traceExporter: withBuiltinAttributesSpanExporter( + this.getSpanExporter(), + builtinAttributes + ), + metricReader: withBuiltinAttributesMetricReader( + this.getMetricReader(), + builtinAttributes + ), textMapPropagator: new CompositePropagator({ propagators: [ new W3CBaggagePropagator(), @@ -81,24 +123,19 @@ export abstract class OpentelemetryFactory { } } -export class LocalOpentelemetryFactory - extends OpentelemetryFactory - implements OnModuleDestroy -{ - private readonly metricsExporter = new PrometheusExporter({ - metricProducers: this.getMetricsProducers(), - }); - - async onModuleDestroy() { - await this.metricsExporter.shutdown(); - } - - override getMetricReader(): MetricReader { - return this.metricsExporter; +export class LocalOpentelemetryFactory extends OpentelemetryFactory { + override getMetricReader() { + return new PeriodicExportingMetricReader({ + // requires jeager service running in 'http://localhost:4318' + // with metrics feature enabled. + // see https://www.jaegertracing.io/docs/1.56/spm + exporter: new OTLPMetricExporter(), + }); } - override getSpanExporter(): SpanExporter { - return new ZipkinExporter(); + override getSpanExporter() { + // requires jeager service running in 'http://localhost:4318' + return new OTLPTraceExporter(); } } diff --git a/packages/backend/server/src/fundamentals/metrics/register.ts b/packages/backend/server/src/fundamentals/metrics/register.ts new file mode 100644 index 000000000000..7df5ef1361f9 --- /dev/null +++ b/packages/backend/server/src/fundamentals/metrics/register.ts @@ -0,0 +1,3 @@ +import { registerInstrumentations } from './instrumentations'; + +registerInstrumentations(); diff --git a/packages/backend/server/src/prelude.ts b/packages/backend/server/src/prelude.ts index 857e8135aa9d..2a8d2f6da058 100644 --- a/packages/backend/server/src/prelude.ts +++ b/packages/backend/server/src/prelude.ts @@ -56,6 +56,7 @@ async function load() { // 6. apply `process.env` map overriding to `globalThis.AFFiNE` applyEnvToConfig(globalThis.AFFiNE); + await import('./fundamentals/metrics/register'); } await load(); diff --git a/tests/affine-cloud/playwright.config.ts b/tests/affine-cloud/playwright.config.ts index 7c7f0d46db4e..75c1775e2ffd 100644 --- a/tests/affine-cloud/playwright.config.ts +++ b/tests/affine-cloud/playwright.config.ts @@ -47,7 +47,7 @@ const config: PlaywrightTestConfig = { DATABASE_URL: process.env.DATABASE_URL ?? 'postgresql://affine:affine@localhost:5432/affine', - NODE_ENV: 'development', + NODE_ENV: 'test', AFFINE_ENV: process.env.AFFINE_ENV ?? 'dev', DEBUG: 'affine:*', FORCE_COLOR: 'true', diff --git a/tests/affine-desktop-cloud/playwright.config.ts b/tests/affine-desktop-cloud/playwright.config.ts index d6227be2fc0e..b29729239159 100644 --- a/tests/affine-desktop-cloud/playwright.config.ts +++ b/tests/affine-desktop-cloud/playwright.config.ts @@ -44,7 +44,7 @@ const config: PlaywrightTestConfig = { DATABASE_URL: process.env.DATABASE_URL ?? 'postgresql://affine:affine@localhost:5432/affine', - NODE_ENV: 'development', + NODE_ENV: 'test', AFFINE_ENV: process.env.AFFINE_ENV ?? 'dev', DEBUG: 'affine:*', FORCE_COLOR: 'true', diff --git a/yarn.lock b/yarn.lock index 80515e589178..9a27785c6ef6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -680,8 +680,8 @@ __metadata: "@node-rs/jsonwebtoken": "npm:^0.5.2" "@opentelemetry/api": "npm:^1.8.0" "@opentelemetry/core": "npm:^1.24.1" - "@opentelemetry/exporter-prometheus": "npm:^0.51.1" - "@opentelemetry/exporter-zipkin": "npm:^1.24.1" + "@opentelemetry/exporter-metrics-otlp-proto": "npm:^0.51.1" + "@opentelemetry/exporter-trace-otlp-proto": "npm:^0.51.1" "@opentelemetry/host-metrics": "npm:^0.35.1" "@opentelemetry/instrumentation": "npm:^0.51.1" "@opentelemetry/instrumentation-graphql": "npm:^0.40.0" @@ -695,7 +695,7 @@ __metadata: "@opentelemetry/sdk-trace-node": "npm:^1.24.1" "@opentelemetry/semantic-conventions": "npm:^1.24.1" "@prisma/client": "npm:^5.12.1" - "@prisma/instrumentation": "npm:^5.12.1" + "@prisma/instrumentation": "npm:^5.14" "@socket.io/redis-adapter": "npm:^8.3.0" "@types/cookie-parser": "npm:^1.4.7" "@types/engine.io": "npm:^3.1.10" @@ -9478,16 +9478,35 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-prometheus@npm:^0.51.1": +"@opentelemetry/exporter-metrics-otlp-http@npm:0.51.1": version: 0.51.1 - resolution: "@opentelemetry/exporter-prometheus@npm:0.51.1" + resolution: "@opentelemetry/exporter-metrics-otlp-http@npm:0.51.1" dependencies: "@opentelemetry/core": "npm:1.24.1" + "@opentelemetry/otlp-exporter-base": "npm:0.51.1" + "@opentelemetry/otlp-transformer": "npm:0.51.1" + "@opentelemetry/resources": "npm:1.24.1" + "@opentelemetry/sdk-metrics": "npm:1.24.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10/f00326920df46cf360413c5aa20537dc043d63dd338919b34db1154e49a77be4adb3a1070e92a3c62caf901ac097dcc343c2846ed257f1cd61377ac4b70f468d + languageName: node + linkType: hard + +"@opentelemetry/exporter-metrics-otlp-proto@npm:^0.51.1": + version: 0.51.1 + resolution: "@opentelemetry/exporter-metrics-otlp-proto@npm:0.51.1" + dependencies: + "@opentelemetry/core": "npm:1.24.1" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.51.1" + "@opentelemetry/otlp-exporter-base": "npm:0.51.1" + "@opentelemetry/otlp-proto-exporter-base": "npm:0.51.1" + "@opentelemetry/otlp-transformer": "npm:0.51.1" "@opentelemetry/resources": "npm:1.24.1" "@opentelemetry/sdk-metrics": "npm:1.24.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/bcc25ff9f7aa6e096852316a6083a4fed2dcd4c8ecc7b4066822f282c8cc7b9f7f8138c48b8729814c37ed757c3cac3b67c52d825fed1b8ed028888c922c0ed0 + checksum: 10/82610c2a68abaa721c05566b5a05a19e8c4a0522f8d37c9739a6f7512dbf1f697f2a804282b80bba7d9883000b6abe1bb4179b4f8a10b39f0bbdfb7a57726510 languageName: node linkType: hard @@ -9522,7 +9541,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-proto@npm:0.51.1": +"@opentelemetry/exporter-trace-otlp-proto@npm:0.51.1, @opentelemetry/exporter-trace-otlp-proto@npm:^0.51.1": version: 0.51.1 resolution: "@opentelemetry/exporter-trace-otlp-proto@npm:0.51.1" dependencies: @@ -9538,7 +9557,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-zipkin@npm:1.24.1, @opentelemetry/exporter-zipkin@npm:^1.24.1": +"@opentelemetry/exporter-zipkin@npm:1.24.1": version: 1.24.1 resolution: "@opentelemetry/exporter-zipkin@npm:1.24.1" dependencies: @@ -10163,7 +10182,7 @@ __metadata: languageName: node linkType: hard -"@prisma/instrumentation@npm:^5.12.1": +"@prisma/instrumentation@npm:^5.14": version: 5.14.0 resolution: "@prisma/instrumentation@npm:5.14.0" dependencies: