@@ -11,6 +11,8 @@ import {
1111 Tracer ,
1212 diag ,
1313 trace ,
14+ metrics ,
15+ Meter ,
1416} from "@opentelemetry/api" ;
1517import { logs , SeverityNumber } from "@opentelemetry/api-logs" ;
1618import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http" ;
@@ -19,6 +21,12 @@ import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs
1921import { type Instrumentation , registerInstrumentations } from "@opentelemetry/instrumentation" ;
2022import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express" ;
2123import { HttpInstrumentation } from "@opentelemetry/instrumentation-http" ;
24+ import {
25+ MeterProvider ,
26+ ConsoleMetricExporter ,
27+ PeriodicExportingMetricReader ,
28+ } from "@opentelemetry/sdk-metrics" ;
29+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http" ;
2230import { Resource } from "@opentelemetry/resources" ;
2331import {
2432 BatchSpanProcessor ,
@@ -30,17 +38,24 @@ import {
3038 TraceIdRatioBasedSampler ,
3139} from "@opentelemetry/sdk-trace-base" ;
3240import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node" ;
33- import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions" ;
41+ import {
42+ SEMRESATTRS_SERVICE_INSTANCE_ID ,
43+ SEMRESATTRS_SERVICE_NAME ,
44+ } from "@opentelemetry/semantic-conventions" ;
3445import { PrismaInstrumentation } from "@prisma/instrumentation" ;
3546import { env } from "~/env.server" ;
3647import { AuthenticatedEnvironment } from "~/services/apiAuth.server" ;
3748import { singleton } from "~/utils/singleton" ;
3849import { LoggerSpanExporter } from "./telemetry/loggerExporter.server" ;
3950import { logger } from "~/services/logger.server" ;
4051import { flattenAttributes } from "@trigger.dev/core/v3" ;
52+ import { randomUUID } from "node:crypto" ;
53+ import { prisma } from "~/db.server" ;
4154
4255export const SEMINTATTRS_FORCE_RECORDING = "forceRecording" ;
4356
57+ const SERVICE_INSTANCE_ID = randomUUID ( ) ;
58+
4459class CustomWebappSampler implements Sampler {
4560 constructor ( private readonly _baseSampler : Sampler ) { }
4661
@@ -83,7 +98,12 @@ class CustomWebappSampler implements Sampler {
8398 }
8499}
85100
86- export const { tracer, logger : otelLogger , provider } = singleton ( "tracer" , getTracer ) ;
101+ export const {
102+ tracer,
103+ logger : otelLogger ,
104+ provider,
105+ meter,
106+ } = singleton ( "opentelemetry" , setupTelemetry ) ;
87107
88108export async function startActiveSpan < T > (
89109 name : string ,
@@ -148,14 +168,15 @@ export async function emitWarnLog(message: string, params: Record<string, unknow
148168 } ) ;
149169}
150170
151- function getTracer ( ) {
171+ function setupTelemetry ( ) {
152172 if ( env . INTERNAL_OTEL_TRACE_DISABLED === "1" ) {
153173 console . log ( `🔦 Tracer disabled, returning a noop tracer` ) ;
154174
155175 return {
156176 tracer : trace . getTracer ( "trigger.dev" , "3.3.12" ) ,
157177 logger : logs . getLogger ( "trigger.dev" , "3.3.12" ) ,
158178 provider : new NodeTracerProvider ( ) ,
179+ meter : setupMetrics ( ) ,
159180 } ;
160181 }
161182
@@ -167,6 +188,7 @@ function getTracer() {
167188 forceFlushTimeoutMillis : 15_000 ,
168189 resource : new Resource ( {
169190 [ SEMRESATTRS_SERVICE_NAME ] : env . SERVICE_NAME ,
191+ [ SEMRESATTRS_SERVICE_INSTANCE_ID ] : SERVICE_INSTANCE_ID ,
170192 } ) ,
171193 sampler : new ParentBasedSampler ( {
172194 root : new CustomWebappSampler ( new TraceIdRatioBasedSampler ( samplingRate ) ) ,
@@ -261,10 +283,90 @@ function getTracer() {
261283 return {
262284 tracer : provider . getTracer ( "trigger.dev" , "3.3.12" ) ,
263285 logger : logs . getLogger ( "trigger.dev" , "3.3.12" ) ,
286+ meter : setupMetrics ( ) ,
264287 provider,
265288 } ;
266289}
267290
291+ function setupMetrics ( ) {
292+ if ( env . INTERNAL_OTEL_METRIC_EXPORTER_DISABLED === "1" ) {
293+ return metrics . getMeter ( "trigger.dev" , "3.3.12" ) ;
294+ }
295+
296+ const exporter = env . INTERNAL_OTEL_METRIC_EXPORTER_URL
297+ ? new OTLPMetricExporter ( {
298+ url : env . INTERNAL_OTEL_METRIC_EXPORTER_URL ,
299+ timeoutMillis : 30_000 ,
300+ headers : parseInternalMetricsHeaders ( ) ?? { } ,
301+ } )
302+ : new ConsoleMetricExporter ( ) ;
303+
304+ const meterProvider = new MeterProvider ( {
305+ resource : new Resource ( {
306+ [ SEMRESATTRS_SERVICE_NAME ] : env . SERVICE_NAME ,
307+ [ SEMRESATTRS_SERVICE_INSTANCE_ID ] : SERVICE_INSTANCE_ID ,
308+ } ) ,
309+ readers : [
310+ new PeriodicExportingMetricReader ( {
311+ exporter,
312+ exportIntervalMillis : env . INTERNAL_OTEL_METRIC_EXPORTER_INTERVAL ,
313+ exportTimeoutMillis : 30_000 ,
314+ } ) ,
315+ ] ,
316+ } ) ;
317+
318+ metrics . setGlobalMeterProvider ( meterProvider ) ;
319+
320+ const meter = meterProvider . getMeter ( "trigger.dev" , "3.3.12" ) ;
321+
322+ configurePrismaMetrics ( meter ) ;
323+
324+ return meter ;
325+ }
326+
327+ function configurePrismaMetrics ( meter : Meter ) {
328+ const totalGauge = meter . createObservableGauge ( "db.pool.connections.total" , {
329+ description : "Open Prisma-pool connections" ,
330+ unit : "connections" ,
331+ } ) ;
332+ const busyGauge = meter . createObservableGauge ( "db.pool.connections.busy" , {
333+ description : "Connections currently executing queries" ,
334+ unit : "connections" ,
335+ } ) ;
336+ const freeGauge = meter . createObservableGauge ( "db.pool.connections.free" , {
337+ description : "Idle (free) connections in the pool" ,
338+ unit : "connections" ,
339+ } ) ;
340+
341+ // Single helper so we hit Prisma only once per scrape ---------------------
342+ async function readPoolCounters ( ) {
343+ const { gauges } = await prisma . $metrics . json ( ) ;
344+
345+ const busy = gauges . find ( ( g ) => g . key === "prisma_pool_connections_busy" ) ?. value ?? 0 ;
346+ const free = gauges . find ( ( g ) => g . key === "prisma_pool_connections_idle" ) ?. value ?? 0 ;
347+ const total =
348+ gauges . find ( ( g ) => g . key === "prisma_pool_connections_open" ) ?. value ?? busy + free ; // fallback compute
349+
350+ return { total, busy, free } ;
351+ }
352+
353+ // Register callbacks (one scrape == one DB call) --------------------------
354+ totalGauge . addCallback ( async ( res ) => {
355+ const { total } = await readPoolCounters ( ) ;
356+ res . observe ( total ) ;
357+ } ) ;
358+
359+ busyGauge . addCallback ( async ( res ) => {
360+ const { busy } = await readPoolCounters ( ) ;
361+ res . observe ( busy ) ;
362+ } ) ;
363+
364+ freeGauge . addCallback ( async ( res ) => {
365+ const { free } = await readPoolCounters ( ) ;
366+ res . observe ( free ) ;
367+ } ) ;
368+ }
369+
268370const SemanticEnvResources = {
269371 ENV_ID : "$trigger.env.id" ,
270372 ENV_TYPE : "$trigger.env.type" ,
@@ -300,3 +402,13 @@ function parseInternalTraceHeaders(): Record<string, string> | undefined {
300402 return ;
301403 }
302404}
405+
406+ function parseInternalMetricsHeaders ( ) : Record < string , string > | undefined {
407+ try {
408+ return env . INTERNAL_OTEL_METRIC_EXPORTER_AUTH_HEADERS
409+ ? ( JSON . parse ( env . INTERNAL_OTEL_METRIC_EXPORTER_AUTH_HEADERS ) as Record < string , string > )
410+ : undefined ;
411+ } catch {
412+ return ;
413+ }
414+ }
0 commit comments