3c7c62c79f
## Summary **All OTel metrics in twenty-server have been silently dropped since April 30.** ### Root cause PR #20149 (`bump @sentry/profiling-node 10.27→10.51`) pulled in `@sentry/node@10.51.0`, which declares `@opentelemetry/api: ^1.9.1` as a **dependency** (not peer). Yarn installed it as a **nested** copy at `1.9.1`, while the hoisted copy stayed at `1.9.0`. At startup in `instrument.ts`: 1. `Sentry.init()` uses the **nested `1.9.1`** to register `trace`, `propagation`, `context` on the OTel global → global version becomes **`1.9.1`** 2. `setGlobalMeterProvider()` uses the **hoisted `1.9.0`** → `registerGlobal` sees version mismatch (`1.9.1` ≠ `1.9.0`) → **silently returns `false`** 3. Global stays `NoopMeterProvider` → every counter, gauge, and histogram in the server is a no-op ### What this PR does 1. **Reverts three troubleshooting PRs** that are no longer needed now that the root cause is identified: - #20230 — heartbeat gauge - #20228 — OTLP export lifecycle logs - #20221 — Sentry revert to 10.27 (which never actually downgraded in `yarn.lock` since `^10.27.0` resolved to `10.51.0`) 2. **Fixes the root cause**: - Root Yarn resolution pinning `@opentelemetry/api` to `1.9.1` → single copy in the entire tree, Sentry and Twenty share the same instance - Named import in `instrument.ts` (`import { metrics as otelMetrics }` instead of default import) as defense-in-depth against CJS interop issues ### Verified on dev cluster Exec'd into the running pod and confirmed: - `@sentry/node` nests `@opentelemetry/api@1.9.1`, hoisted is `1.9.0` - `Sentry.init()` → global version `1.9.1` → `setGlobalMeterProvider` with VERSION `1.9.0` → returns `false` → `NoopMeterProvider` - Same-version registration returns `true` → `MeterProvider` ✓ ## Test plan - [ ] CI passes (lint, typecheck, build) - [ ] Deploy to dev cluster and verify metrics flow to collector - [ ] Confirm `node_modules/@opentelemetry/api/package.json` shows `1.9.1` with no nested copy under `@sentry/` --------- Co-authored-by: Cursor <cursoragent@cursor.com>
106 lines
3.1 KiB
TypeScript
106 lines
3.1 KiB
TypeScript
import process from 'process';
|
|
|
|
import { metrics as otelMetrics } from '@opentelemetry/api';
|
|
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
|
|
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
|
|
import {
|
|
AggregationTemporality,
|
|
ConsoleMetricExporter,
|
|
MeterProvider,
|
|
PeriodicExportingMetricReader,
|
|
} from '@opentelemetry/sdk-metrics';
|
|
import * as Sentry from '@sentry/node';
|
|
import { nodeProfilingIntegration } from '@sentry/profiling-node';
|
|
|
|
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
|
|
|
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
|
|
import { MeterDriver } from 'src/engine/core-modules/metrics/types/meter-driver.type';
|
|
import { parseArrayEnvVar } from 'src/utils/parse-array-env-var';
|
|
|
|
const meterDrivers = parseArrayEnvVar(
|
|
process.env.METER_DRIVER,
|
|
Object.values(MeterDriver),
|
|
[],
|
|
);
|
|
|
|
if (process.env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.SENTRY) {
|
|
Sentry.init({
|
|
environment: process.env.SENTRY_ENVIRONMENT,
|
|
release: process.env.APP_VERSION,
|
|
dsn: process.env.SENTRY_DSN,
|
|
integrations: [
|
|
Sentry.redisIntegration(),
|
|
Sentry.httpIntegration(),
|
|
Sentry.expressIntegration(),
|
|
Sentry.graphqlIntegration(),
|
|
Sentry.postgresIntegration(),
|
|
Sentry.vercelAIIntegration({
|
|
recordInputs: true,
|
|
recordOutputs: true,
|
|
}),
|
|
nodeProfilingIntegration(),
|
|
],
|
|
tracesSampleRate: 0.1,
|
|
profilesSampleRate: 0.3,
|
|
sendDefaultPii: true,
|
|
debug: process.env.NODE_ENV === NodeEnvironment.DEVELOPMENT,
|
|
beforeSendSpan: (span) => {
|
|
const twentyContext = Sentry.getIsolationScope().getScopeData().contexts
|
|
?.twenty as
|
|
| {
|
|
workspace_id?: string;
|
|
user_workspace_id?: string;
|
|
}
|
|
| undefined;
|
|
|
|
if (!twentyContext?.workspace_id) {
|
|
return span;
|
|
}
|
|
|
|
span.data = {
|
|
...span.data,
|
|
'twenty.workspace.id': twentyContext.workspace_id,
|
|
...(twentyContext.user_workspace_id && {
|
|
'twenty.user_workspace.id': twentyContext.user_workspace_id,
|
|
}),
|
|
};
|
|
|
|
return span;
|
|
},
|
|
});
|
|
}
|
|
|
|
// Meter setup
|
|
|
|
const prometheusExporter = meterDrivers.includes(MeterDriver.Prometheus)
|
|
? new PrometheusExporter({ port: 9464 })
|
|
: null;
|
|
|
|
const meterProvider = new MeterProvider({
|
|
readers: [
|
|
...(meterDrivers.includes(MeterDriver.Console)
|
|
? [
|
|
new PeriodicExportingMetricReader({
|
|
exporter: new ConsoleMetricExporter(),
|
|
exportIntervalMillis: 10000,
|
|
}),
|
|
]
|
|
: []),
|
|
...(meterDrivers.includes(MeterDriver.OpenTelemetry)
|
|
? [
|
|
new PeriodicExportingMetricReader({
|
|
exporter: new OTLPMetricExporter({
|
|
url: process.env.OTLP_COLLECTOR_METRICS_ENDPOINT_URL,
|
|
temporalityPreference: AggregationTemporality.DELTA,
|
|
}),
|
|
exportIntervalMillis: 10000,
|
|
}),
|
|
]
|
|
: []),
|
|
...(prometheusExporter ? [prometheusExporter] : []),
|
|
],
|
|
});
|
|
|
|
otelMetrics.setGlobalMeterProvider(meterProvider);
|