@platformatic/telemetry 2.12.0 → 2.14.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.
package/eslint.config.js CHANGED
@@ -1,3 +1,9 @@
1
1
  'use strict'
2
2
 
3
- module.exports = require('neostandard')({})
3
+ const neostandard = require('neostandard')
4
+
5
+ module.exports = neostandard(
6
+ {
7
+ ignores: ['**/.next', '**/dist', '**/tmp', 'test/fixtures/**'],
8
+ }
9
+ )
package/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  'use strict'
2
2
 
3
3
  const telemetry = require('./lib/telemetry')
4
+ const { createTelemetryThreadInterceptorHooks } = require('./lib/thread-interceptor-hooks')
4
5
  const schema = require('./lib/schema')
5
6
 
6
7
  module.exports = {
7
8
  telemetry,
9
+ createTelemetryThreadInterceptorHooks,
8
10
  schema,
9
11
  }
@@ -0,0 +1,55 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ ExportResultCode,
5
+ hrTimeToMicroseconds,
6
+ } = require('@opentelemetry/core')
7
+ const path = require('node:path')
8
+ const { appendFileSync } = require('node:fs')
9
+
10
+ // Export spans to a file, mostly for testing purposes.
11
+ class FileSpanExporter {
12
+ #path
13
+ constructor (opts) {
14
+ if (!opts.path) {
15
+ this.#path = path.resolve('spans.log')
16
+ } else {
17
+ this.#path = opts.path
18
+ }
19
+ }
20
+
21
+ export (spans, resultCallback) {
22
+ for (const span of spans) {
23
+ appendFileSync(this.#path, JSON.stringify(this.#exportInfo(span)) + '\n')
24
+ }
25
+ resultCallback(ExportResultCode.SUCCESS)
26
+ }
27
+
28
+ shutdown () {
29
+ return this.forceFlush()
30
+ }
31
+
32
+ forceFlush () {
33
+ return Promise.resolve()
34
+ }
35
+
36
+ #exportInfo (span) {
37
+ return {
38
+ traceId: span.spanContext().traceId,
39
+ parentId: span.parentSpanId,
40
+ traceState: span.spanContext().traceState?.serialize(),
41
+ name: span.name,
42
+ id: span.spanContext().spanId,
43
+ kind: span.kind,
44
+ timestamp: hrTimeToMicroseconds(span.startTime),
45
+ duration: hrTimeToMicroseconds(span.duration),
46
+ attributes: span.attributes,
47
+ status: span.status,
48
+ events: span.events,
49
+ links: span.links,
50
+ resource: span.resource,
51
+ }
52
+ }
53
+ }
54
+
55
+ module.exports = FileSpanExporter
@@ -1,9 +1,8 @@
1
1
  'use strict'
2
2
  const process = require('node:process')
3
3
  const opentelemetry = require('@opentelemetry/sdk-node')
4
- const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http')
5
4
  const { Resource } = require('@opentelemetry/resources')
6
- const setupTelemetry = require('./telemetry-config')
5
+ const FileSpanExporter = require('./file-span-exporter')
7
6
  const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions')
8
7
  const { workerData } = require('node:worker_threads')
9
8
  const { resolve } = require('node:path')
@@ -12,11 +11,64 @@ const logger = require('abstract-logging')
12
11
  const { statSync, readFileSync } = require('node:fs') // We want to have all this synch
13
12
  const util = require('node:util')
14
13
  const debuglog = util.debuglog('@platformatic/telemetry')
14
+ const {
15
+ ConsoleSpanExporter,
16
+ BatchSpanProcessor,
17
+ SimpleSpanProcessor,
18
+ InMemorySpanExporter,
19
+ } = require('@opentelemetry/sdk-trace-base')
20
+ const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http')
21
+
22
+ // See: https://www.npmjs.com/package/@opentelemetry/instrumentation-http
23
+ // When this is fixed we should set this to 'http' and fixe the tests
24
+ // https://github.com/open-telemetry/opentelemetry-js/issues/5103
25
+ process.env.OTEL_SEMCONV_STABILITY_OPT_IN = 'http/dup'
15
26
 
16
27
  const setupNodeHTTPTelemetry = (opts) => {
17
28
  const { serviceName } = opts
18
- debuglog(`Setting up Node.js Open Telemetry instrumentation for service: ${serviceName}`)
19
- const { spanProcessors } = setupTelemetry(opts, logger)
29
+
30
+ let exporter = opts.exporter
31
+ if (!exporter) {
32
+ logger.warn('No exporter configured, defaulting to console.')
33
+ exporter = { type: 'console' }
34
+ }
35
+ const exporters = Array.isArray(exporter) ? exporter : [exporter]
36
+ const spanProcessors = []
37
+ for (const exporter of exporters) {
38
+ // Exporter config:
39
+ // https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_exporter_zipkin.ExporterConfig.html
40
+ const exporterOptions = { ...exporter.options, serviceName }
41
+
42
+ let exporterObj
43
+ if (exporter.type === 'console') {
44
+ exporterObj = new ConsoleSpanExporter(exporterOptions)
45
+ } else if (exporter.type === 'otlp') {
46
+ const {
47
+ OTLPTraceExporter,
48
+ } = require('@opentelemetry/exporter-trace-otlp-proto')
49
+ exporterObj = new OTLPTraceExporter(exporterOptions)
50
+ } else if (exporter.type === 'zipkin') {
51
+ const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin')
52
+ exporterObj = new ZipkinExporter(exporterOptions)
53
+ } else if (exporter.type === 'memory') {
54
+ exporterObj = new InMemorySpanExporter()
55
+ } else if (exporter.type === 'file') {
56
+ exporterObj = new FileSpanExporter(exporterOptions)
57
+ } else {
58
+ logger.warn(
59
+ `Unknown exporter type: ${exporter.type}, defaulting to console.`
60
+ )
61
+ exporterObj = new ConsoleSpanExporter(exporterOptions)
62
+ }
63
+
64
+ // We use a SimpleSpanProcessor for the console/memory exporters and a BatchSpanProcessor for the others.
65
+ // (these are the ones used by tests)
66
+ const spanProcessor = ['memory', 'console', 'file'].includes(exporter.type)
67
+ ? new SimpleSpanProcessor(exporterObj)
68
+ : new BatchSpanProcessor(exporterObj)
69
+ spanProcessors.push(spanProcessor)
70
+ }
71
+
20
72
  const sdk = new opentelemetry.NodeSDK({
21
73
  spanProcessors, // https://github.com/open-telemetry/opentelemetry-js/issues/4881#issuecomment-2358059714
22
74
  instrumentations: [
package/lib/schema.js CHANGED
@@ -5,7 +5,7 @@ const ExporterSchema = {
5
5
  properties: {
6
6
  type: {
7
7
  type: 'string',
8
- enum: ['console', 'otlp', 'zipkin', 'memory'],
8
+ enum: ['console', 'otlp', 'zipkin', 'memory', 'file'],
9
9
  default: 'console',
10
10
  },
11
11
  options: {
@@ -20,6 +20,10 @@ const ExporterSchema = {
20
20
  type: 'object',
21
21
  description: 'Headers to send to the exporter. Not used for console or memory exporters.',
22
22
  },
23
+ path: {
24
+ type: 'string',
25
+ description: 'The path to write the traces to. Only for file exporter.'
26
+ }
23
27
  },
24
28
  },
25
29
  additionalProperties: false,
@@ -1,20 +1,90 @@
1
1
  'use strict'
2
2
 
3
+ const { SpanStatusCode, SpanKind } = require('@opentelemetry/api')
4
+ const { PlatformaticContext } = require('./platformatic-context')
5
+ const {
6
+ fastifyTextMapGetter,
7
+ fastifyTextMapSetter,
8
+ } = require('./fastify-text-map')
9
+ const { formatParamUrl } = require('@fastify/swagger')
10
+ const fastUri = require('fast-uri')
11
+ const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions')
3
12
  const {
4
13
  ConsoleSpanExporter,
5
14
  BatchSpanProcessor,
6
15
  SimpleSpanProcessor,
7
16
  InMemorySpanExporter,
8
17
  } = require('@opentelemetry/sdk-trace-base')
9
- const {
10
- SemanticResourceAttributes,
11
- } = require('@opentelemetry/semantic-conventions')
18
+
19
+ const FileSpanExporter = require('./file-span-exporter')
20
+
12
21
  const { Resource } = require('@opentelemetry/resources')
13
22
  const { PlatformaticTracerProvider } = require('./platformatic-trace-provider')
14
23
 
15
24
  const { name: moduleName, version: moduleVersion } = require('../package.json')
16
25
 
17
- const setupTelemetry = (opts, logger) => {
26
+ // Platformatic telemetry plugin.
27
+ // Supported Exporters:
28
+ // - console
29
+ // - otlp: (which also supports jaeger, see: https://opentelemetry.io/docs/instrumentation/js/exporters/#otlp-endpoint)
30
+ // - zipkin (https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-exporter-zipkin/README.md)
31
+ // - memory: for testing
32
+
33
+ // This has been partially copied and modified from @autotelic/opentelemetry: https://github.com/autotelic/fastify-opentelemetry/blob/main/index.js
34
+ // , according with [MIT license](https://github.com/autotelic/fastify-opentelemetry/blob/main/LICENSE.md):
35
+ // MIT License
36
+ // Copyright (c) 2020 autotelic
37
+ // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
38
+ // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
39
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
40
+ const extractPath = (request) => {
41
+ // We must user RouterPath, because otherwise `/test/123` will be considered as
42
+ // a different operation than `/test/321`. In case is not set (this should actually happen only for HTTP/404) we fallback to the path.
43
+ const { routeOptions, url } = request
44
+ let path
45
+ const routerPath = routeOptions && routeOptions.url
46
+ if (routerPath) {
47
+ path = formatParamUrl(routerPath)
48
+ } else {
49
+ path = url
50
+ }
51
+ return path
52
+ }
53
+
54
+ function formatSpanName (request, path) {
55
+ const { method } = request
56
+ /* istanbul ignore next */
57
+ return path ? `${method} ${path}` : method
58
+ }
59
+
60
+ const formatSpanAttributes = {
61
+ request (request, path) {
62
+ const { hostname, method, url, protocol = 'http' } = request
63
+ // Inspired by: https://github.com/fastify/fastify-url-data/blob/master/plugin.js#L11
64
+ const urlData = fastUri.parse(`${protocol}://${hostname}${url}`)
65
+ return {
66
+ 'server.address': hostname,
67
+ 'server.port': urlData.port,
68
+ 'http.request.method': method,
69
+ 'url.path': path,
70
+ 'url.scheme': protocol,
71
+ }
72
+ },
73
+ reply (reply) {
74
+ return {
75
+ 'http.response.status_code': reply.statusCode,
76
+ }
77
+ },
78
+ error (error) {
79
+ return {
80
+ 'error.name': error.name,
81
+ 'error.message': error.message,
82
+ 'error.stack': error.stack,
83
+ }
84
+ },
85
+ }
86
+
87
+ const initTelemetry = (opts, logger) => {
18
88
  const { serviceName, version } = opts
19
89
  let exporter = opts.exporter
20
90
  if (!exporter) {
@@ -30,8 +100,8 @@ const setupTelemetry = (opts, logger) => {
30
100
 
31
101
  const provider = new PlatformaticTracerProvider({
32
102
  resource: new Resource({
33
- [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
34
- [SemanticResourceAttributes.SERVICE_VERSION]: version,
103
+ [ATTR_SERVICE_NAME]: serviceName,
104
+ [ATTR_SERVICE_VERSION]: version,
35
105
  }),
36
106
  })
37
107
 
@@ -56,6 +126,8 @@ const setupTelemetry = (opts, logger) => {
56
126
  exporterObj = new ZipkinExporter(exporterOptions)
57
127
  } else if (exporter.type === 'memory') {
58
128
  exporterObj = new InMemorySpanExporter()
129
+ } else if (exporter.type === 'file') {
130
+ exporterObj = new FileSpanExporter(exporterOptions)
59
131
  } else {
60
132
  logger.warn(
61
133
  `Unknown exporter type: ${exporter.type}, defaulting to console.`
@@ -64,7 +136,8 @@ const setupTelemetry = (opts, logger) => {
64
136
  }
65
137
 
66
138
  // We use a SimpleSpanProcessor for the console/memory exporters and a BatchSpanProcessor for the others.
67
- const spanProcessor = ['memory', 'console'].includes(exporter.type)
139
+ // (these are the ones used by tests)
140
+ const spanProcessor = ['memory', 'console', 'file'].includes(exporter.type)
68
141
  ? new SimpleSpanProcessor(exporterObj)
69
142
  : new BatchSpanProcessor(exporterObj)
70
143
  spanProcessors.push(spanProcessor)
@@ -78,4 +151,208 @@ const setupTelemetry = (opts, logger) => {
78
151
  return { tracer, exporters: exporterObjs, propagator, provider, spanProcessors }
79
152
  }
80
153
 
81
- module.exports = setupTelemetry
154
+ function setupTelemetry (opts, logger) {
155
+ const openTelemetryAPIs = initTelemetry(opts, logger)
156
+ const { tracer, propagator, provider } = openTelemetryAPIs
157
+ const skipOperations =
158
+ opts?.skip?.map((skip) => {
159
+ const { method, path } = skip
160
+ return `${method}${path}`
161
+ }) || []
162
+
163
+ const startHTTPSpan = async (request, reply) => {
164
+ if (skipOperations.includes(`${request.method}${request.url}`)) {
165
+ request.log.debug(
166
+ { operation: `${request.method}${request.url}` },
167
+ 'Skipping telemetry'
168
+ )
169
+ return
170
+ }
171
+
172
+ // We populate the context with the incoming request headers
173
+ let context = propagator.extract(
174
+ new PlatformaticContext(),
175
+ request,
176
+ fastifyTextMapGetter
177
+ )
178
+
179
+ const path = extractPath(request)
180
+ const span = tracer.startSpan(formatSpanName(request, path), {}, context)
181
+ span.kind = SpanKind.SERVER
182
+ // Next 2 lines are needed by W3CTraceContextPropagator
183
+ context = context.setSpan(span)
184
+ span.setAttributes(formatSpanAttributes.request(request, path))
185
+ span.context = context
186
+ // Inject the propagation headers
187
+ propagator.inject(context, reply, fastifyTextMapSetter)
188
+ request.span = span
189
+ }
190
+
191
+ const setErrorInSpan = async (request, _reply, error) => {
192
+ const span = request.span
193
+ span.setAttributes(formatSpanAttributes.error(error))
194
+ }
195
+
196
+ const endHTTPSpan = async (request, reply) => {
197
+ const span = request.span
198
+ if (span) {
199
+ propagator.inject(span.context, reply, fastifyTextMapSetter)
200
+ const spanStatus = { code: SpanStatusCode.OK }
201
+ if (reply.statusCode >= 400) {
202
+ spanStatus.code = SpanStatusCode.ERROR
203
+ }
204
+ span.setAttributes(formatSpanAttributes.reply(reply))
205
+ span.setStatus(spanStatus)
206
+ span.end()
207
+ }
208
+ }
209
+
210
+ //* Client APIs
211
+ const getSpanPropagationHeaders = (span) => {
212
+ const context = span.context
213
+ const headers = {}
214
+ propagator.inject(context, headers, {
215
+ set (_carrier, key, value) {
216
+ headers[key] = value
217
+ },
218
+ })
219
+ return headers
220
+ }
221
+
222
+ // If a context is passed, is used. Otherwise is a new one is created.
223
+ // Note that in this case we don't set the span in request, as this is a client call.
224
+ // So the client caller is responible of:
225
+ // - setting the propagatorHeaders in the client request
226
+ // - closing the span
227
+ const startHTTPSpanClient = (url, method, ctx) => {
228
+ let context = ctx || new PlatformaticContext()
229
+ const urlObj = fastUri.parse(url)
230
+
231
+ if (skipOperations.includes(`${method}${urlObj.path}`)) {
232
+ logger.debug(
233
+ { operation: `${method}${urlObj.path}` },
234
+ 'Skipping telemetry'
235
+ )
236
+ return
237
+ }
238
+
239
+ /* istanbul ignore next */
240
+ method = method || ''
241
+ let name
242
+ if (urlObj.port) {
243
+ name = `${method} ${urlObj.scheme}://${urlObj.host}:${urlObj.port}${urlObj.path}`
244
+ } else {
245
+ name = `${method} ${urlObj.scheme}://${urlObj.host}${urlObj.path}`
246
+ }
247
+
248
+ const span = tracer.startSpan(name, {}, context)
249
+ span.kind = SpanKind.CLIENT
250
+
251
+ /* istanbul ignore next */
252
+ const attributes = url
253
+ ? {
254
+ 'server.address': urlObj.host,
255
+ 'server.port': urlObj.port,
256
+ 'http.request.method': method,
257
+ 'url.full': url,
258
+ 'url.path': urlObj.path,
259
+ 'url.scheme': urlObj.scheme,
260
+ }
261
+ : {}
262
+ span.setAttributes(attributes)
263
+
264
+ // Next 2 lines are needed by W3CTraceContextPropagator
265
+ context = context.setSpan(span)
266
+ span.context = context
267
+
268
+ const telemetryHeaders = getSpanPropagationHeaders(span)
269
+ return { span, telemetryHeaders }
270
+ }
271
+
272
+ const endHTTPSpanClient = (span, response) => {
273
+ /* istanbul ignore next */
274
+ if (!span) {
275
+ return
276
+ }
277
+ if (response) {
278
+ const spanStatus = { code: SpanStatusCode.OK }
279
+ if (response.statusCode >= 400) {
280
+ spanStatus.code = SpanStatusCode.ERROR
281
+ }
282
+ span.setAttributes({
283
+ 'http.response.status_code': response.statusCode,
284
+ })
285
+ span.setStatus(spanStatus)
286
+ } else {
287
+ span.setStatus({
288
+ code: SpanStatusCode.ERROR,
289
+ message: 'No response received',
290
+ })
291
+ }
292
+ span.end()
293
+ }
294
+
295
+ const setErrorInSpanClient = (span, error) => {
296
+ /* istanbul ignore next */
297
+ if (!span) {
298
+ return
299
+ }
300
+ span.setAttributes(formatSpanAttributes.error(error))
301
+ }
302
+
303
+ // In the generic "startSpan" the attributes here are specified by the caller
304
+ const startSpan = (name, ctx, attributes = {}, kind = SpanKind.INTERNAL) => {
305
+ const context = ctx || new PlatformaticContext()
306
+ const span = tracer.startSpan(name, {}, context)
307
+ span.kind = kind
308
+ span.setAttributes(attributes)
309
+ span.context = context
310
+ return span
311
+ }
312
+
313
+ const endSpan = (span, error) => {
314
+ /* istanbul ignore next */
315
+ if (!span) {
316
+ return
317
+ }
318
+ if (error) {
319
+ span.setStatus({
320
+ code: SpanStatusCode.ERROR,
321
+ message: error.message,
322
+ })
323
+ } else {
324
+ const spanStatus = { code: SpanStatusCode.OK }
325
+ span.setStatus(spanStatus)
326
+ }
327
+ span.end()
328
+ }
329
+
330
+ // Unfortunately, this must be async, because of: https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_trace_base.SpanProcessor.html#shutdown
331
+ const shutdown = async () => {
332
+ try {
333
+ await provider.shutdown()
334
+ } catch (err) {
335
+ logger.error({ err }, 'Error shutting down telemetry provider')
336
+ }
337
+ }
338
+
339
+ return {
340
+ startHTTPSpan,
341
+ endHTTPSpan,
342
+ setErrorInSpan,
343
+ startHTTPSpanClient,
344
+ endHTTPSpanClient,
345
+ setErrorInSpanClient,
346
+ startSpan,
347
+ endSpan,
348
+ shutdown,
349
+ openTelemetryAPIs,
350
+ }
351
+ }
352
+
353
+ module.exports = {
354
+ setupTelemetry,
355
+ formatSpanName,
356
+ formatSpanAttributes,
357
+ extractPath
358
+ }
package/lib/telemetry.js CHANGED
@@ -1,271 +1,28 @@
1
1
  'use strict'
2
2
 
3
3
  const fp = require('fastify-plugin')
4
- const { SpanStatusCode, SpanKind } = require('@opentelemetry/api')
5
- const { PlatformaticContext } = require('./platformatic-context')
6
- const {
7
- fastifyTextMapGetter,
8
- fastifyTextMapSetter,
9
- } = require('./fastify-text-map')
10
- const setup = require('./telemetry-config')
11
- const { formatParamUrl } = require('@fastify/swagger')
12
- const fastUri = require('fast-uri')
13
-
14
- // Platformatic telemetry plugin.
15
- // Supported Exporters:
16
- // - console
17
- // - otlp: (which also supports jaeger, see: https://opentelemetry.io/docs/instrumentation/js/exporters/#otlp-endpoint)
18
- // - zipkin (https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-exporter-zipkin/README.md)
19
- // - memory: for testing
20
-
21
- // This has been partially copied and modified from @autotelic/opentelemetry: https://github.com/autotelic/fastify-opentelemetry/blob/main/index.js
22
- // , according with [MIT license](https://github.com/autotelic/fastify-opentelemetry/blob/main/LICENSE.md):
23
- // MIT License
24
- // Copyright (c) 2020 autotelic
25
- // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
26
- // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
27
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
- const extractPath = (request) => {
29
- // We must user RouterPath, because otherwise `/test/123` will be considered as
30
- // a different operation than `/test/321`. In case is not set (this should actually happen only for HTTP/404) we fallback to the path.
31
- const { routeOptions, url } = request
32
- let path
33
- const routerPath = routeOptions && routeOptions.url
34
- if (routerPath) {
35
- path = formatParamUrl(routerPath)
36
- } else {
37
- path = url
38
- }
39
- return path
40
- }
41
-
42
- function formatSpanName (request, path) {
43
- const { method } = request
44
- /* istanbul ignore next */
45
- return path ? `${method} ${path}` : method
46
- }
47
-
48
- const formatSpanAttributes = {
49
- request (request, path) {
50
- const { hostname, method, url, protocol } = request
51
- // Inspired by: https://github.com/fastify/fastify-url-data/blob/master/plugin.js#L11
52
- const urlData = fastUri.parse(`${protocol}://${hostname}${url}`)
53
- return {
54
- 'server.address': hostname,
55
- 'server.port': urlData.port,
56
- 'http.request.method': method,
57
- 'url.path': path,
58
- 'url.scheme': protocol,
59
- }
60
- },
61
- reply (reply) {
62
- return {
63
- 'http.response.status_code': reply.statusCode,
64
- }
65
- },
66
- error (error) {
67
- return {
68
- 'error.name': error.name,
69
- 'error.message': error.message,
70
- 'error.stack': error.stack,
71
- }
72
- },
73
- }
74
-
75
- async function setupTelemetry (app, opts) {
76
- const openTelemetryAPIs = setup(opts, app.log)
77
- const { tracer, propagator, provider } = openTelemetryAPIs
78
- const skipOperations =
79
- opts?.skip?.map((skip) => {
80
- const { method, path } = skip
81
- return `${method}${path}`
82
- }) || []
83
-
84
- // expose the span as a request decorator
85
- app.decorateRequest('span')
86
-
87
- const startHTTPSpan = async (request) => {
88
- if (skipOperations.includes(`${request.method}${request.url}`)) {
89
- request.log.debug(
90
- { operation: `${request.method}${request.url}` },
91
- 'Skipping telemetry'
92
- )
93
- return
94
- }
95
-
96
- // We populate the context with the incoming request headers
97
- let context = propagator.extract(
98
- new PlatformaticContext(),
99
- request,
100
- fastifyTextMapGetter
101
- )
102
-
103
- const path = extractPath(request)
104
- const span = tracer.startSpan(formatSpanName(request, path), {}, context)
105
- span.kind = SpanKind.SERVER
106
- // Next 2 lines are needed by W3CTraceContextPropagator
107
- context = context.setSpan(span)
108
- span.setAttributes(formatSpanAttributes.request(request, path))
109
- span.context = context
110
- request.span = span
111
- }
112
-
113
- const setErrorInSpan = async (request, _reply, error) => {
114
- const span = request.span
115
- span.setAttributes(formatSpanAttributes.error(error))
116
- }
117
-
118
- const injectPropagationHeadersInReply = async (request, reply) => {
119
- if (request.span) {
120
- const context = request.span.context
121
- propagator.inject(context, reply, fastifyTextMapSetter)
122
- }
123
- }
124
-
125
- const endHTTPSpan = async (request, reply) => {
126
- const span = request.span
127
- if (span) {
128
- const spanStatus = { code: SpanStatusCode.OK }
129
- if (reply.statusCode >= 400) {
130
- spanStatus.code = SpanStatusCode.ERROR
131
- }
132
- span.setAttributes(formatSpanAttributes.reply(reply))
133
- span.setStatus(spanStatus)
134
- span.end()
135
- }
136
- }
4
+ const { SpanKind } = require('@opentelemetry/api')
5
+ const { setupTelemetry } = require('./telemetry-config')
6
+
7
+ // Telemetry fastify plugin
8
+ async function telemetry (app, opts) {
9
+ const {
10
+ startHTTPSpan,
11
+ endHTTPSpan,
12
+ setErrorInSpan,
13
+ startSpan,
14
+ endSpan,
15
+ startHTTPSpanClient,
16
+ endHTTPSpanClient,
17
+ setErrorInSpanClient,
18
+ shutdown,
19
+ openTelemetryAPIs
20
+ } = setupTelemetry(opts, app.log)
137
21
 
138
22
  app.addHook('onRequest', startHTTPSpan)
139
- app.addHook('onSend', injectPropagationHeadersInReply)
140
23
  app.addHook('onResponse', endHTTPSpan)
141
24
  app.addHook('onError', setErrorInSpan)
142
- app.addHook('onClose', async function () {
143
- try {
144
- await provider.shutdown()
145
- } catch (err) {
146
- app.log.error({ err }, 'Error shutting down telemetry provider')
147
- }
148
- })
149
-
150
- //* Client APIs
151
- const getSpanPropagationHeaders = (span) => {
152
- const context = span.context
153
- const headers = {}
154
- propagator.inject(context, headers, {
155
- set (_carrier, key, value) {
156
- headers[key] = value
157
- },
158
- })
159
- return headers
160
- }
161
-
162
- // If a context is passed, is used. Otherwise is a new one is created.
163
- // Note that in this case we don't set the span in request, as this is a client call.
164
- // So the client caller is responible of:
165
- // - setting the propagatorHeaders in the client request
166
- // - closing the span
167
- const startHTTPSpanClient = (url, method, ctx) => {
168
- let context = ctx || new PlatformaticContext()
169
- const urlObj = fastUri.parse(url)
170
-
171
- if (skipOperations.includes(`${method}${urlObj.path}`)) {
172
- app.log.debug(
173
- { operation: `${method}${urlObj.path}` },
174
- 'Skipping telemetry'
175
- )
176
- return
177
- }
178
-
179
- /* istanbul ignore next */
180
- method = method || ''
181
- let name
182
- if (urlObj.port) {
183
- name = `${method} ${urlObj.scheme}://${urlObj.host}:${urlObj.port}${urlObj.path}`
184
- } else {
185
- name = `${method} ${urlObj.scheme}://${urlObj.host}${urlObj.path}`
186
- }
187
-
188
- const span = tracer.startSpan(name, {}, context)
189
- span.kind = SpanKind.CLIENT
190
-
191
- /* istanbul ignore next */
192
- const attributes = url
193
- ? {
194
- 'server.address': urlObj.host,
195
- 'server.port': urlObj.port,
196
- 'http.request.method': method,
197
- 'url.full': url,
198
- 'url.path': urlObj.path,
199
- 'url.scheme': urlObj.scheme,
200
- }
201
- : {}
202
- span.setAttributes(attributes)
203
-
204
- // Next 2 lines are needed by W3CTraceContextPropagator
205
- context = context.setSpan(span)
206
- span.context = context
207
-
208
- const telemetryHeaders = getSpanPropagationHeaders(span)
209
- return { span, telemetryHeaders }
210
- }
211
-
212
- const endHTTPSpanClient = (span, response) => {
213
- /* istanbul ignore next */
214
- if (!span) {
215
- return
216
- }
217
- if (response) {
218
- const spanStatus = { code: SpanStatusCode.OK }
219
- if (response.statusCode >= 400) {
220
- spanStatus.code = SpanStatusCode.ERROR
221
- }
222
- span.setAttributes({
223
- 'http.response.status_code': response.statusCode,
224
- })
225
- span.setStatus(spanStatus)
226
- } else {
227
- span.setStatus({
228
- code: SpanStatusCode.ERROR,
229
- message: 'No response received',
230
- })
231
- }
232
- span.end()
233
- }
234
-
235
- const setErrorInSpanClient = (span, error) => {
236
- /* istanbul ignore next */
237
- if (!span) {
238
- return
239
- }
240
- span.setAttributes(formatSpanAttributes.error(error))
241
- }
242
-
243
- // In the generic "startSpan" the attributes here are specified by the caller
244
- const startSpan = (name, ctx, attributes = {}, kind = SpanKind.INTERNAL) => {
245
- const context = ctx || new PlatformaticContext()
246
- const span = tracer.startSpan(name, {}, context)
247
- span.kind = kind
248
- span.setAttributes(attributes)
249
- span.context = context
250
- return span
251
- }
252
-
253
- const endSpan = (span, error) => {
254
- /* istanbul ignore next */
255
- if (!span) {
256
- return
257
- }
258
- if (error) {
259
- span.setStatus({
260
- code: SpanStatusCode.ERROR,
261
- message: error.message,
262
- })
263
- } else {
264
- const spanStatus = { code: SpanStatusCode.OK }
265
- span.setStatus(spanStatus)
266
- }
267
- span.end()
268
- }
25
+ app.addHook('onClose', shutdown)
269
26
 
270
27
  app.decorate('openTelemetry', {
271
28
  ...openTelemetryAPIs,
@@ -274,8 +31,9 @@ async function setupTelemetry (app, opts) {
274
31
  setErrorInSpanClient,
275
32
  startSpan,
276
33
  endSpan,
34
+ shutdown,
277
35
  SpanKind,
278
36
  })
279
37
  }
280
38
 
281
- module.exports = fp(setupTelemetry)
39
+ module.exports = fp(telemetry)
@@ -0,0 +1,126 @@
1
+ 'use strict'
2
+
3
+ const { SpanStatusCode, SpanKind } = require('@opentelemetry/api')
4
+ const { formatSpanName, formatSpanAttributes, extractPath } = require('./telemetry-config')
5
+ const api = require('@opentelemetry/api')
6
+ const fastUri = require('fast-uri')
7
+ const packageJson = require('../package.json')
8
+
9
+ const tracer = api.trace.getTracer(packageJson.name, packageJson.version)
10
+
11
+ const createTelemetryThreadInterceptorHooks = () => {
12
+ const onServerRequest = (req, cb) => {
13
+ const activeContext = api.propagation.extract(api.context.active(), req.headers)
14
+
15
+ const path = extractPath(req)
16
+ const span = tracer.startSpan(formatSpanName(req, path), {
17
+ attributes: formatSpanAttributes.request(req, path),
18
+ kind: SpanKind.SERVER
19
+ }, activeContext)
20
+ const ctx = api.trace.setSpan(activeContext, span)
21
+
22
+ api.context.with(ctx, cb)
23
+ }
24
+
25
+ const onServerResponse = (_req, _res) => {
26
+ const activeContext = api.context.active()
27
+ const span = api.trace.getSpan(activeContext)
28
+ if (span) {
29
+ span.end()
30
+ }
31
+ }
32
+
33
+ const onServerError = (_req, _res, error) => {
34
+ const activeContext = api.context.active()
35
+ const span = api.trace.getSpan(activeContext)
36
+ if (span) {
37
+ span.setAttributes(formatSpanAttributes.error(error))
38
+ }
39
+ }
40
+
41
+ const onClientRequest = (req, ctx) => {
42
+ const activeContext = api.context.active()
43
+
44
+ const { origin, method = '', path } = req
45
+ const targetUrl = `${origin}${path}`
46
+ const urlObj = fastUri.parse(targetUrl)
47
+
48
+ let name
49
+ if (urlObj.port) {
50
+ name = `${method} ${urlObj.scheme}://${urlObj.host}:${urlObj.port}${urlObj.path}`
51
+ } else {
52
+ name = `${method} ${urlObj.scheme}://${urlObj.host}${urlObj.path}`
53
+ }
54
+ const span = tracer.startSpan(name, {
55
+ attributes: {
56
+ 'server.address': urlObj.host,
57
+ 'server.port': urlObj.port,
58
+ 'http.request.method': method,
59
+ 'url.full': targetUrl,
60
+ 'url.path': urlObj.path,
61
+ 'url.scheme': urlObj.scheme,
62
+ },
63
+ kind: SpanKind.CLIENT
64
+ }, activeContext)
65
+
66
+ // Headers propagation
67
+ const headers = {}
68
+ // This line is important, otherwise it will use the old context
69
+ const newCtx = api.trace.setSpan(activeContext, span)
70
+ api.propagation.inject(newCtx, headers, {
71
+ set (_carrier, key, value) {
72
+ headers[key] = value
73
+ },
74
+ })
75
+ req.headers = {
76
+ ...req.headers,
77
+ ...headers
78
+ }
79
+
80
+ ctx.span = span
81
+ }
82
+
83
+ const onClientResponse = (_req, res, ctx) => {
84
+ const span = ctx.span ?? null
85
+ if (!span) {
86
+ return
87
+ }
88
+ if (res) {
89
+ const spanStatus = { code: SpanStatusCode.OK }
90
+ if (res.statusCode >= 400) {
91
+ spanStatus.code = SpanStatusCode.ERROR
92
+ }
93
+ span.setAttributes({
94
+ 'http.response.status_code': res.statusCode,
95
+ })
96
+ span.setStatus(spanStatus)
97
+ } else {
98
+ span.setStatus({
99
+ code: SpanStatusCode.ERROR,
100
+ message: 'No response received',
101
+ })
102
+ }
103
+ span.end()
104
+ }
105
+
106
+ const onClientError = (_req, _res, ctx, error) => {
107
+ const span = ctx.span ?? null
108
+ if (!span) {
109
+ return
110
+ }
111
+ span.setAttributes(formatSpanAttributes.error(error))
112
+ }
113
+
114
+ return {
115
+ onServerRequest,
116
+ onServerResponse,
117
+ onServerError,
118
+ onClientRequest,
119
+ onClientResponse,
120
+ onClientError
121
+ }
122
+ }
123
+
124
+ module.exports = {
125
+ createTelemetryThreadInterceptorHooks
126
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/telemetry",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "OpenTelemetry integration for Platformatic",
5
5
  "main": "index.js",
6
6
  "author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",
@@ -19,20 +19,20 @@
19
19
  "dependencies": {
20
20
  "@fastify/swagger": "^9.0.0",
21
21
  "@opentelemetry/api": "^1.8.0",
22
- "@opentelemetry/auto-instrumentations-node": "^0.52.0",
22
+ "@opentelemetry/instrumentation-http": "^0.54.2",
23
23
  "@opentelemetry/core": "^1.22.0",
24
24
  "@opentelemetry/exporter-trace-otlp-proto": "^0.54.0",
25
25
  "@opentelemetry/exporter-zipkin": "^1.22.0",
26
26
  "@opentelemetry/resources": "^1.22.0",
27
27
  "@opentelemetry/sdk-node": "^0.54.0",
28
28
  "@opentelemetry/sdk-trace-base": "^1.22.0",
29
- "@opentelemetry/semantic-conventions": "^1.22.0",
29
+ "@opentelemetry/semantic-conventions": "^1.27.0",
30
30
  "abstract-logging": "^2.0.1",
31
31
  "fast-uri": "^2.3.0",
32
32
  "fastify-plugin": "^5.0.0"
33
33
  },
34
34
  "scripts": {
35
- "test": "npm run lint && borp",
35
+ "test": "npm run lint && borp --timeout=60000 --concurrency=1",
36
36
  "lint": "eslint"
37
37
  }
38
38
  }