@platformatic/telemetry 3.29.0 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,14 @@ export class FileSpanExporter {
26
26
  }
27
27
 
28
28
  #exportInfo (span) {
29
+ // OpenTelemetry 2.0+ resources need to be serialized with their attributes
30
+ // The resource.attributes property contains a map of attribute values
31
+ // We need to convert it to the format expected by tests (_rawAttributes array)
32
+ const resource = {
33
+ attributes: span.resource?.attributes || {},
34
+ _rawAttributes: Object.entries(span.resource?.attributes || {})
35
+ }
36
+
29
37
  return {
30
38
  traceId: span.spanContext().traceId,
31
39
  // parentId has been removed from otel 2.0, we need to get it from parentSpanContext
@@ -43,7 +51,7 @@ export class FileSpanExporter {
43
51
  status: span.status,
44
52
  events: span.events,
45
53
  links: span.links,
46
- resource: span.resource,
54
+ resource,
47
55
  // instrumentationLibrary is deprecated in otel 2.0, we need to use instrumentationScope
48
56
  instrumentationScope: span.instrumentationLibrary || span.instrumentationScope
49
57
  }
@@ -1,85 +1,60 @@
1
+ import { context, propagation } from '@opentelemetry/api'
2
+ import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
3
+ import { registerInstrumentations } from '@opentelemetry/instrumentation'
1
4
  import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'
2
5
  import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici'
6
+ import { W3CTraceContextPropagator } from '@opentelemetry/core'
7
+ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
3
8
  import { resourceFromAttributes } from '@opentelemetry/resources'
4
- import * as opentelemetry from '@opentelemetry/sdk-node'
5
- import {
6
- BatchSpanProcessor,
7
- ConsoleSpanExporter,
8
- InMemorySpanExporter,
9
- SimpleSpanProcessor
10
- } from '@opentelemetry/sdk-trace-base'
11
9
  import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
12
- import { abstractLogger } from '@platformatic/foundation'
13
10
  import { AsyncLocalStorage } from 'node:async_hooks'
14
11
  import { readFileSync, statSync } from 'node:fs'
15
- import { createRequire } from 'node:module'
16
12
  import { tmpdir } from 'node:os'
17
13
  import { resolve } from 'node:path'
18
14
  import process from 'node:process'
19
15
  import util from 'node:util'
20
16
  import { workerData } from 'node:worker_threads'
21
- import { FileSpanExporter } from './file-span-exporter.js'
22
17
  import { getInstrumentations } from './pluggable-instrumentations.js'
18
+ import { getSpanProcessors } from './span-processors.js'
23
19
 
24
20
  const debuglog = util.debuglog('@platformatic/telemetry')
25
- const require = createRequire(import.meta.url)
26
21
 
27
22
  // See: https://www.npmjs.com/package/@opentelemetry/instrumentation-http
28
- // When this is fixed we should set this to 'http' and fixe the tests
23
+ // When this is fixed we should set this to 'http' and fix the tests
29
24
  // https://github.com/open-telemetry/opentelemetry-js/issues/5103
30
25
  process.env.OTEL_SEMCONV_STABILITY_OPT_IN = 'http/dup'
31
26
 
27
+ // Set up global propagator EARLY for trace context propagation via HTTP headers
28
+ // This must be set before any HTTP operations occur
29
+ propagation.setGlobalPropagator(new W3CTraceContextPropagator())
30
+
31
+ // Set up global context manager for async context propagation
32
+ // Context manager MUST be global for instrumentations to work
33
+ const contextManager = new AsyncLocalStorageContextManager()
34
+ contextManager.enable()
35
+ context.setGlobalContextManager(contextManager)
36
+
32
37
  const setupNodeHTTPTelemetry = async (opts, applicationDir) => {
33
38
  const { applicationName, instrumentations = [] } = opts
34
39
  const additionalInstrumentations = await getInstrumentations(instrumentations, applicationDir)
35
40
 
36
- let exporter = opts.exporter
37
- if (!exporter) {
38
- abstractLogger.warn('No exporter configured, defaulting to console.')
39
- exporter = { type: 'console' }
40
- }
41
- const exporters = Array.isArray(exporter) ? exporter : [exporter]
42
- const spanProcessors = []
43
- for (const exporter of exporters) {
44
- // Exporter config:
45
- // https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_exporter_zipkin.ExporterConfig.html
46
- const exporterOptions = { ...exporter.options, applicationName }
47
-
48
- let exporterObj
49
- if (exporter.type === 'console') {
50
- exporterObj = new ConsoleSpanExporter(exporterOptions)
51
- } else if (exporter.type === 'otlp') {
52
- const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto')
53
- exporterObj = new OTLPTraceExporter(exporterOptions)
54
- } else if (exporter.type === 'zipkin') {
55
- const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin')
56
- exporterObj = new ZipkinExporter(exporterOptions)
57
- } else if (exporter.type === 'memory') {
58
- exporterObj = new InMemorySpanExporter()
59
- } else if (exporter.type === 'file') {
60
- exporterObj = new FileSpanExporter(exporterOptions)
61
- } else {
62
- abstractLogger.warn(`Unknown exporter type: ${exporter.type}, defaulting to console.`)
63
- exporterObj = new ConsoleSpanExporter(exporterOptions)
64
- }
65
-
66
- let spanProcessor
67
- // We use a SimpleSpanProcessor for the console/memory exporters and a BatchSpanProcessor for the others.
68
- // , unless "processor" is set to "simple" (used only in tests)
69
- if (exporter.processor === 'simple' || ['memory', 'console', 'file'].includes(exporter.type)) {
70
- spanProcessor = new SimpleSpanProcessor(exporterObj)
71
- } else {
72
- spanProcessor = new BatchSpanProcessor(exporterObj)
73
- }
74
- spanProcessors.push(spanProcessor)
75
- }
41
+ const { spanProcessors } = getSpanProcessors(opts)
76
42
 
77
43
  const clientSpansAls = new AsyncLocalStorage()
78
44
  globalThis.platformatic = globalThis.platformatic || {}
79
45
  globalThis.platformatic.clientSpansAls = clientSpansAls
80
46
 
81
- const sdk = new opentelemetry.NodeSDK({
47
+ const tracerProvider = new NodeTracerProvider({
82
48
  spanProcessors, // https://github.com/open-telemetry/opentelemetry-js/issues/4881#issuecomment-2358059714
49
+ resource: resourceFromAttributes({
50
+ [ATTR_SERVICE_NAME]: applicationName
51
+ })
52
+ })
53
+ globalThis.platformatic.tracerProvider = tracerProvider
54
+
55
+ // Register instrumentations with our TracerProvider
56
+ registerInstrumentations({
57
+ tracerProvider,
83
58
  instrumentations: [
84
59
  new UndiciInstrumentation({
85
60
  responseHook: span => {
@@ -91,18 +66,16 @@ const setupNodeHTTPTelemetry = async (opts, applicationDir) => {
91
66
  }),
92
67
  new HttpInstrumentation(),
93
68
  ...additionalInstrumentations
94
- ],
95
- resource: resourceFromAttributes({
96
- [ATTR_SERVICE_NAME]: applicationName
97
- })
69
+ ]
98
70
  })
99
- sdk.start()
100
71
 
101
- process.on('SIGTERM', () => {
102
- sdk
103
- .shutdown()
104
- .then(() => debuglog('Tracing terminated'))
105
- .catch(error => debuglog('Error terminating tracing', error))
72
+ process.on('SIGTERM', async () => {
73
+ try {
74
+ await tracerProvider.shutdown()
75
+ debuglog('Tracing terminated')
76
+ } catch (error) {
77
+ debuglog('Error terminating tracing', error)
78
+ }
106
79
  })
107
80
  }
108
81
 
@@ -0,0 +1,65 @@
1
+ import {
2
+ BatchSpanProcessor,
3
+ ConsoleSpanExporter,
4
+ InMemorySpanExporter,
5
+ SimpleSpanProcessor
6
+ } from '@opentelemetry/sdk-trace-base'
7
+ import { abstractLogger } from '@platformatic/foundation'
8
+ import { createRequire } from 'node:module'
9
+ import { FileSpanExporter } from './file-span-exporter.js'
10
+
11
+ const require = createRequire(import.meta.url)
12
+
13
+ export function getSpanProcessors (opts = {}, logger = abstractLogger) {
14
+ const { applicationName, version } = opts
15
+
16
+ // Set up exporters
17
+ let exporter = opts.exporter
18
+ if (!exporter) {
19
+ logger.warn?.('No exporter configured, defaulting to console.')
20
+ exporter = { type: 'console' }
21
+ }
22
+
23
+ const exporters = Array.isArray(exporter) ? exporter : [exporter]
24
+
25
+ logger.debug?.(
26
+ `Setting up platformatic telemetry for application: ${applicationName}${version ? ' version: ' + version : ''} with exporter of type ${exporter.type}`
27
+ )
28
+
29
+ const exporterObjs = []
30
+ const spanProcessors = []
31
+ for (const exp of exporters) {
32
+ const exporterOptions = { ...exp.options, applicationName }
33
+
34
+ let exporterObj
35
+ if (exp.type === 'console') {
36
+ exporterObj = new ConsoleSpanExporter(exporterOptions)
37
+ } else if (exp.type === 'otlp') {
38
+ const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto')
39
+ exporterObj = new OTLPTraceExporter(exporterOptions)
40
+ } else if (exp.type === 'zipkin') {
41
+ const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin')
42
+ exporterObj = new ZipkinExporter(exporterOptions)
43
+ } else if (exp.type === 'memory') {
44
+ exporterObj = new InMemorySpanExporter()
45
+ } else if (exp.type === 'file') {
46
+ exporterObj = new FileSpanExporter(exporterOptions)
47
+ } else {
48
+ logger.warn?.(`Unknown exporter type: ${exp.type}, defaulting to console.`)
49
+ exporterObj = new ConsoleSpanExporter(exporterOptions)
50
+ }
51
+ exporterObjs.push(exporterObj)
52
+
53
+ let spanProcessor
54
+ // We use a SimpleSpanProcessor for the console/memory exporters and a BatchSpanProcessor for the others.
55
+ // unless "processor" is set to "simple" (used only in tests)
56
+ if (exp.processor === 'simple' || ['memory', 'console', 'file'].includes(exp.type)) {
57
+ spanProcessor = new SimpleSpanProcessor(exporterObj)
58
+ } else {
59
+ spanProcessor = new BatchSpanProcessor(exporterObj)
60
+ }
61
+ spanProcessors.push(spanProcessor)
62
+ }
63
+
64
+ return { exporters: exporterObjs, spanProcessors }
65
+ }
@@ -1,19 +1,12 @@
1
1
  import { formatParamUrl } from '@fastify/swagger'
2
2
  import { SpanKind, SpanStatusCode } from '@opentelemetry/api'
3
3
  import { resourceFromAttributes } from '@opentelemetry/resources'
4
- import {
5
- BatchSpanProcessor,
6
- ConsoleSpanExporter,
7
- InMemorySpanExporter,
8
- SimpleSpanProcessor
9
- } from '@opentelemetry/sdk-trace-base'
10
4
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
11
5
  import fastUri from 'fast-uri'
12
- import { createRequire } from 'node:module'
13
6
  import { fastifyTextMapGetter, fastifyTextMapSetter } from './fastify-text-map.js'
14
- import { FileSpanExporter } from './file-span-exporter.js'
15
7
  import { PlatformaticContext } from './platformatic-context.js'
16
8
  import { PlatformaticTracerProvider } from './platformatic-trace-provider.js'
9
+ import { getSpanProcessors } from './span-processors.js'
17
10
 
18
11
  import { name as moduleName, version as moduleVersion } from './version.js'
19
12
 
@@ -78,18 +71,8 @@ export const formatSpanAttributes = {
78
71
  }
79
72
 
80
73
  const initTelemetry = (opts, logger) => {
74
+ const { exporters, spanProcessors } = getSpanProcessors(opts, logger)
81
75
  const { applicationName, version } = opts
82
- let exporter = opts.exporter
83
- if (!exporter) {
84
- logger.warn('No exporter configured, defaulting to console.')
85
- exporter = { type: 'console' }
86
- }
87
-
88
- const exporters = Array.isArray(exporter) ? exporter : [exporter]
89
-
90
- logger.debug(
91
- `Setting up platformatic telemetry for application: ${applicationName}${version ? ' version: ' + version : ''} with exporter of type ${exporter.type}`
92
- )
93
76
 
94
77
  const provider = new PlatformaticTracerProvider({
95
78
  resource: resourceFromAttributes({
@@ -98,50 +81,11 @@ const initTelemetry = (opts, logger) => {
98
81
  })
99
82
  })
100
83
 
101
- const exporterObjs = []
102
- const spanProcessors = []
103
- const require = createRequire(import.meta.url)
104
- for (const exporter of exporters) {
105
- // Exporter config:
106
- // https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_exporter_zipkin.ExporterConfig.html
107
- const exporterOptions = { ...exporter.options, applicationName }
108
-
109
- let exporterObj
110
- if (exporter.type === 'console') {
111
- exporterObj = new ConsoleSpanExporter(exporterOptions)
112
- } else if (exporter.type === 'otlp') {
113
- // We require here because this require (and only the require!) creates some issue with c8 on some mjs tests on other modules. Since we need an assignemet here, we don't use a switch.
114
- const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto')
115
- exporterObj = new OTLPTraceExporter(exporterOptions)
116
- } else if (exporter.type === 'zipkin') {
117
- const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin')
118
- exporterObj = new ZipkinExporter(exporterOptions)
119
- } else if (exporter.type === 'memory') {
120
- exporterObj = new InMemorySpanExporter()
121
- } else if (exporter.type === 'file') {
122
- exporterObj = new FileSpanExporter(exporterOptions)
123
- } else {
124
- logger.warn(`Unknown exporter type: ${exporter.type}, defaulting to console.`)
125
- exporterObj = new ConsoleSpanExporter(exporterOptions)
126
- }
127
-
128
- let spanProcessor
129
- // We use a SimpleSpanProcessor for the console/memory exporters and a BatchSpanProcessor for the others.
130
- // , unless "processor" is set to "simple" (used only in tests)
131
- if (exporter.processor === 'simple' || ['memory', 'console', 'file'].includes(exporter.type)) {
132
- spanProcessor = new SimpleSpanProcessor(exporterObj)
133
- } else {
134
- spanProcessor = new BatchSpanProcessor(exporterObj)
135
- }
136
- spanProcessors.push(spanProcessor)
137
- exporterObjs.push(exporterObj)
138
- }
139
-
140
84
  provider.addSpanProcessor(spanProcessors)
141
85
  const tracer = provider.getTracer(moduleName, moduleVersion)
142
86
  const propagator = provider.getPropagator()
143
87
 
144
- return { tracer, exporters: exporterObjs, propagator, provider, spanProcessors }
88
+ return { tracer, exporters, propagator, provider, spanProcessors }
145
89
  }
146
90
 
147
91
  export function setupTelemetry (opts, logger) {
@@ -3,14 +3,22 @@ import fastUri from 'fast-uri'
3
3
  import { formatSpanAttributes, formatSpanName } from './telemetry-config.js'
4
4
  import { name as moduleName, version as moduleVersion } from './version.js'
5
5
 
6
- const tracer = trace.getTracer(moduleName, moduleVersion)
6
+ // Get tracer from our isolated TracerProvider stored in globalThis.platformatic
7
+ function getTracer () {
8
+ const tracerProvider = globalThis.platformatic?.tracerProvider
9
+ if (tracerProvider) {
10
+ return tracerProvider.getTracer(moduleName, moduleVersion)
11
+ }
12
+ // Fallback to global tracer if our provider isn't set up yet
13
+ return trace.getTracer(moduleName, moduleVersion)
14
+ }
7
15
 
8
16
  export function createTelemetryThreadInterceptorHooks () {
9
17
  const onServerRequest = (req, cb) => {
10
18
  const activeContext = propagation.extract(context.active(), req.headers)
11
19
 
12
20
  const route = req.routeOptions?.url ?? null
13
- const span = tracer.startSpan(
21
+ const span = getTracer().startSpan(
14
22
  formatSpanName(req, route),
15
23
  {
16
24
  attributes: formatSpanAttributes.request(req, route),
@@ -52,7 +60,7 @@ export function createTelemetryThreadInterceptorHooks () {
52
60
  } else {
53
61
  name = `${method} ${urlObj.scheme}://${urlObj.host}${urlObj.path}`
54
62
  }
55
- const span = tracer.startSpan(
63
+ const span = getTracer().startSpan(
56
64
  name,
57
65
  {
58
66
  attributes: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/telemetry",
3
- "version": "3.29.0",
3
+ "version": "3.30.0",
4
4
  "description": "OpenTelemetry integration for Platformatic",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "@opentelemetry/instrumentation-pg": "^0.55.0",
17
17
  "cleaner-spec-reporter": "^0.5.0",
18
18
  "express": "^5.1.0",
19
- "fastify": "^5.4.0",
19
+ "fastify": "^5.7.0",
20
20
  "neostandard": "^0.12.2",
21
21
  "protobufjs": "^7.5.3",
22
22
  "typescript": "^5.9.2"
@@ -36,7 +36,7 @@
36
36
  "@opentelemetry/semantic-conventions": "1.36.0",
37
37
  "fast-uri": "^3.0.6",
38
38
  "fastify-plugin": "^5.0.1",
39
- "@platformatic/foundation": "3.29.0"
39
+ "@platformatic/foundation": "3.30.0"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=22.19.0"