@@ -2,6 +2,7 @@ interface OTelGlobalState {
22 __otelMetricsShutdown ?: ( ) => Promise < void >
33}
44
5+ import fs from 'node:fs'
56import { vi } from 'vitest'
67
78const mockedMetricsModules = [
@@ -16,6 +17,8 @@ const mockedMetricsModules = [
1617 '@opentelemetry/instrumentation-runtime-node' ,
1718 '@opentelemetry/resources' ,
1819 '@opentelemetry/sdk-metrics' ,
20+ '@platformatic/globals' ,
21+ 'os' ,
1922] as const
2023
2124async function importOtelMetricsModule ( ) {
@@ -262,4 +265,142 @@ describe('otel metrics', () => {
262265 } )
263266 )
264267 } )
268+
269+ test ( 'uses Watt worker identity as the OTel service instance id' , async ( ) => {
270+ delete process . env . OTEL_EXPORTER_OTLP_ENDPOINT
271+ delete process . env . OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
272+
273+ const registerInstrumentations = vi . fn ( ( ) => vi . fn ( ) )
274+ const resourceFromAttributes = vi . fn ( ( attributes ) => attributes )
275+ const HostMetrics = vi . fn ( function ( ) {
276+ return {
277+ start : vi . fn ( ) ,
278+ }
279+ } )
280+ const MeterProvider = vi . fn ( function ( ) {
281+ return {
282+ shutdown : vi . fn ( ) . mockResolvedValue ( undefined ) ,
283+ getMeter : vi . fn ( ( ) => ( { } ) ) ,
284+ }
285+ } )
286+ const prometheusExporterOptions : Array < { withResourceConstantLabels : RegExp } > = [ ]
287+ const PrometheusExporter = vi . fn ( function ( options : { withResourceConstantLabels : RegExp } ) {
288+ prometheusExporterOptions . push ( options )
289+ return {
290+ getMetricsRequestHandler : vi . fn ( ) ,
291+ }
292+ } )
293+ const RuntimeNodeInstrumentation = vi . fn ( function ( ) {
294+ return { }
295+ } )
296+ const StorageNodeInstrumentation = vi . fn ( function ( ) {
297+ return { }
298+ } )
299+
300+ vi . doMock ( '../../config' , ( ) => ( {
301+ getConfig : vi . fn ( ( ) => ( {
302+ version : 'test-version' ,
303+ otelMetricsExportIntervalMs : 1000 ,
304+ otelMetricsEnabled : true ,
305+ otelMetricsTemporality : 'CUMULATIVE' ,
306+ prometheusMetricsEnabled : true ,
307+ region : 'local' ,
308+ serviceName : 'storage-api' ,
309+ } ) ) ,
310+ } ) )
311+ vi . doMock ( '@platformatic/globals' , ( ) => ( {
312+ getGlobal : vi . fn ( ( ) => ( {
313+ applicationId : 'storage-api:tenant' ,
314+ workerId : '3' ,
315+ } ) ) ,
316+ } ) )
317+ vi . doMock ( 'os' , ( ) => ( {
318+ hostname : vi . fn ( ( ) => 'storage-host-a' ) ,
319+ } ) )
320+ vi . doMock ( '@internal/monitoring/logger' , ( ) => ( {
321+ logger : { info : vi . fn ( ) } ,
322+ logSchema : { error : vi . fn ( ) , info : vi . fn ( ) } ,
323+ } ) )
324+ vi . doMock ( '@internal/monitoring/system' , ( ) => ( {
325+ StorageNodeInstrumentation,
326+ } ) )
327+ vi . doMock ( '@opentelemetry/api' , ( ) => ( {
328+ metrics : {
329+ setGlobalMeterProvider : vi . fn ( ) ,
330+ } ,
331+ } ) )
332+ vi . doMock ( '@opentelemetry/exporter-metrics-otlp-grpc' , ( ) => ( {
333+ OTLPMetricExporter : vi . fn ( function ( ) {
334+ return { }
335+ } ) ,
336+ } ) )
337+ vi . doMock ( '@opentelemetry/exporter-prometheus' , ( ) => ( {
338+ PrometheusExporter,
339+ } ) )
340+ vi . doMock ( '@opentelemetry/host-metrics' , ( ) => ( {
341+ HostMetrics,
342+ } ) )
343+ vi . doMock ( '@opentelemetry/instrumentation' , ( ) => ( {
344+ registerInstrumentations,
345+ } ) )
346+ vi . doMock ( '@opentelemetry/instrumentation-runtime-node' , ( ) => ( {
347+ RuntimeNodeInstrumentation,
348+ } ) )
349+ vi . doMock ( '@opentelemetry/resources' , ( ) => ( {
350+ resourceFromAttributes,
351+ } ) )
352+ vi . doMock ( '@opentelemetry/sdk-metrics' , ( ) => ( {
353+ AggregationTemporality : {
354+ CUMULATIVE : 'CUMULATIVE' ,
355+ DELTA : 'DELTA' ,
356+ } ,
357+ AggregationType : {
358+ DROP : 'DROP' ,
359+ EXPLICIT_BUCKET_HISTOGRAM : 'EXPLICIT_BUCKET_HISTOGRAM' ,
360+ } ,
361+ MeterProvider,
362+ PeriodicExportingMetricReader : vi . fn ( ) ,
363+ } ) )
364+
365+ await importOtelMetricsModule ( )
366+
367+ expect ( resourceFromAttributes ) . toHaveBeenCalledWith (
368+ expect . objectContaining ( {
369+ instance : 'storage-host-a' ,
370+ 'service.instance.id' : 'storage-host-a:storage-api:tenant:worker:3' ,
371+ 'platformatic.application.id' : 'storage-api:tenant' ,
372+ 'worker.id' : '3' ,
373+ 'process.pid' : process . pid ,
374+ } )
375+ )
376+ expect ( PrometheusExporter ) . toHaveBeenCalledWith (
377+ expect . objectContaining ( {
378+ withResourceConstantLabels : expect . objectContaining ( {
379+ test : expect . any ( Function ) ,
380+ } ) ,
381+ } )
382+ )
383+
384+ const prometheusLabelFilter = prometheusExporterOptions [ 0 ] . withResourceConstantLabels
385+ expect ( prometheusLabelFilter . test ( 'service.instance.id' ) ) . toBe ( true )
386+ expect ( prometheusLabelFilter . test ( 'worker.id' ) ) . toBe ( true )
387+ expect ( prometheusLabelFilter . test ( 'platformatic.application.id' ) ) . toBe ( true )
388+ } )
389+
390+ test ( 'collector promotes service instance identity to metric labels' , ( ) => {
391+ const collectorConfig = fs . readFileSync (
392+ 'monitoring/otel/config/otel-collector-config.yml' ,
393+ 'utf8'
394+ )
395+
396+ expect ( collectorConfig ) . toContain (
397+ 'set(attributes["service_instance_id"], resource.attributes["service.instance.id"])'
398+ )
399+ expect ( collectorConfig ) . toContain (
400+ 'set(attributes["worker_id"], resource.attributes["worker.id"])'
401+ )
402+ expect ( collectorConfig ) . toContain (
403+ 'set(attributes["platformatic_application_id"], resource.attributes["platformatic.application.id"])'
404+ )
405+ } )
265406} )
0 commit comments