From a3cf565ba3f0b0b44c2be455579424556a4ddd40 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 4 May 2026 12:30:59 +0200 Subject: [PATCH] feat(server): add heartbeat gauge + first-export-attempt log for OTLP (#20230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds a trivial always-on observable gauge (`twenty.heartbeat = 1`) so the OTLP exporter fires on every 10s collection tick, even on idle pods. Without this, `PeriodicExportingMetricReader` skips the export when `scopeMetrics` is empty, so the process-log wrapper never runs and we can't prove OTLP connectivity. - Adds a one-time "first periodic export attempt" log line inside the wrapped exporter, completing the startup log sequence: `OTLP reader enabled` → `first periodic export attempt` → `first export ok` / `export failed`. Co-authored-by: Cursor --- packages/twenty-server/src/instrument.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/twenty-server/src/instrument.ts b/packages/twenty-server/src/instrument.ts index 99f2781315..83ee3da9bb 100644 --- a/packages/twenty-server/src/instrument.ts +++ b/packages/twenty-server/src/instrument.ts @@ -70,12 +70,19 @@ const serializeOtlExportFailure = (error: unknown): string => { }; let otlpMetricsFirstExportSuccessLogged = false; +let otlpMetricsFirstExportAttemptLogged = false; const wrapOtlMetricExporterWithProcessLogs = ( inner: OTLPMetricExporter, otlpEndpointForLog: string, ): PushMetricExporter => ({ export(metrics: ResourceMetrics, resultCallback) { + if (!otlpMetricsFirstExportAttemptLogged) { + otlpMetricsFirstExportAttemptLogged = true; + console.log( + `${OTLP_METRICS_LOG_PREFIX} first periodic export attempt | endpoint=${otlpEndpointForLog}`, + ); + } const totalMetricData = metrics.scopeMetrics.reduce( (accumulator, scopeMetric) => accumulator + scopeMetric.metrics.length, 0, @@ -177,3 +184,19 @@ const meterProvider = new MeterProvider({ }); opentelemetry.metrics.setGlobalMeterProvider(meterProvider); + +// Always-on gauge so the OTLP exporter fires on every collection tick, +// even when the process is otherwise idle. This guarantees the process-log +// wrapper above can prove connectivity (first export ok / export failed). +if (meterDrivers.includes(MeterDriver.OpenTelemetry)) { + const heartbeatMeter = opentelemetry.metrics.getMeter( + 'twenty-server-heartbeat', + ); + const heartbeat = heartbeatMeter.createObservableGauge('twenty.heartbeat', { + description: 'Always-on gauge (1) to prove OTLP export pipeline', + }); + + heartbeat.addCallback((observableResult) => { + observableResult.observe(1); + }); +}