@platformatic/telemetry 3.4.1 → 3.5.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/NOTICE CHANGED
@@ -11,3 +11,8 @@
11
11
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
+
15
+ The files in `test/otelserver/opentelemetry` has been created by the OpenTelemetry community and are licensed under the Apache License 2.0. The original files can be found at
16
+ https://github.com/open-telemetry/opentelemetry-proto/tree/1608f92cf08119f9aec237c910b200d1317ec696/opentelemetry
17
+
18
+
package/eslint.config.js CHANGED
@@ -1,3 +1,5 @@
1
- 'use strict'
1
+ import neostandard from 'neostandard'
2
2
 
3
- module.exports = require('neostandard')({})
3
+ export default neostandard({
4
+ ignores: ['**/.next', '**/dist', '**/tmp', 'test/fixtures/**']
5
+ })
package/index.js CHANGED
@@ -1,11 +1,3 @@
1
- 'use strict'
2
-
3
- const telemetry = require('./lib/telemetry')
4
- const schema = require('./lib/schema')
5
- const setupNodeHTTPTelemetry = require('./lib/node-http-telemetry')
6
-
7
- module.exports = {
8
- telemetry,
9
- schema,
10
- setupNodeHTTPTelemetry
11
- }
1
+ export * as schema from './lib/schema.js'
2
+ export * as telemetry from './lib/telemetry.js'
3
+ export { createTelemetryThreadInterceptorHooks } from './lib/thread-interceptor-hooks.js'
@@ -1,22 +1,15 @@
1
- 'use strict'
2
-
3
- const fastifyTextMapGetter = {
1
+ export const fastifyTextMapGetter = {
4
2
  get (request, key) {
5
3
  return request.headers[key]
6
4
  },
7
5
  /* istanbul ignore next */
8
6
  keys (request) {
9
7
  return Object.keys(request.headers)
10
- },
8
+ }
11
9
  }
12
10
 
13
- const fastifyTextMapSetter = {
11
+ export const fastifyTextMapSetter = {
14
12
  set (reply, key, value) {
15
13
  reply.headers({ [key]: value })
16
- },
17
- }
18
-
19
- module.exports = {
20
- fastifyTextMapGetter,
21
- fastifyTextMapSetter,
14
+ }
22
15
  }
@@ -0,0 +1,51 @@
1
+ import { ExportResultCode, hrTimeToMicroseconds } from '@opentelemetry/core'
2
+ import { appendFileSync } from 'node:fs'
3
+ import { resolve as resolvePath } from 'node:path'
4
+ import { workerData } from 'node:worker_threads'
5
+
6
+ // Export spans to a file, mostly for testing purposes.
7
+ export class FileSpanExporter {
8
+ #path
9
+ constructor (opts) {
10
+ this.#path = resolvePath(workerData?.dirname ?? process.cwd(), opts.path ?? 'spans.log')
11
+ }
12
+
13
+ export (spans, resultCallback) {
14
+ for (const span of spans) {
15
+ appendFileSync(this.#path, JSON.stringify(this.#exportInfo(span)) + '\n')
16
+ }
17
+ resultCallback(ExportResultCode.SUCCESS)
18
+ }
19
+
20
+ shutdown () {
21
+ return this.forceFlush()
22
+ }
23
+
24
+ forceFlush () {
25
+ return Promise.resolve()
26
+ }
27
+
28
+ #exportInfo (span) {
29
+ return {
30
+ traceId: span.spanContext().traceId,
31
+ // parentId has been removed from otel 2.0, we need to get it from parentSpanContext
32
+ parentSpanContext: {
33
+ traceId: span.parentSpanContext?.traceId,
34
+ spanId: span.parentSpanContext?.spanId
35
+ },
36
+ traceState: span.spanContext().traceState?.serialize(),
37
+ name: span.name,
38
+ id: span.spanContext().spanId,
39
+ kind: span.kind,
40
+ timestamp: hrTimeToMicroseconds(span.startTime),
41
+ duration: hrTimeToMicroseconds(span.duration),
42
+ attributes: span.attributes,
43
+ status: span.status,
44
+ events: span.events,
45
+ links: span.links,
46
+ resource: span.resource,
47
+ // instrumentationLibrary is deprecated in otel 2.0, we need to use instrumentationScope
48
+ instrumentationScope: span.instrumentationLibrary || span.instrumentationScope
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,14 @@
1
+ import { createRequire } from 'node:module'
2
+ import { join } from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ export async function importOrLocal ({ projectDir, pkg }) {
6
+ try {
7
+ return import(pkg)
8
+ } catch (err) {
9
+ const pkgJsonPath = join(projectDir, 'package.json')
10
+ const _require = createRequire(pkgJsonPath)
11
+ const fileToImport = _require.resolve(pkg)
12
+ return import(pathToFileURL(fileToImport))
13
+ }
14
+ }
@@ -1,8 +1,6 @@
1
- 'use strict'
2
-
3
1
  // This implements the SpanProcessor interface:
4
2
  // https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/SpanProcessor.ts
5
- class MultiSpanProcessor {
3
+ export class MultiSpanProcessor {
6
4
  constructor (_spanProcessors = []) {
7
5
  this._spanProcessors = _spanProcessors
8
6
  }
@@ -35,5 +33,3 @@ class MultiSpanProcessor {
35
33
  return Promise.all(promises)
36
34
  }
37
35
  }
38
-
39
- module.exports = { MultiSpanProcessor }
@@ -0,0 +1,142 @@
1
+ import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'
2
+ import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici'
3
+ 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
+ import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
12
+ import { abstractLogger } from '@platformatic/foundation'
13
+ import { AsyncLocalStorage } from 'node:async_hooks'
14
+ import { readFileSync, statSync } from 'node:fs'
15
+ import { createRequire } from 'node:module'
16
+ import { tmpdir } from 'node:os'
17
+ import { resolve } from 'node:path'
18
+ import process from 'node:process'
19
+ import util from 'node:util'
20
+ import { workerData } from 'node:worker_threads'
21
+ import { FileSpanExporter } from './file-span-exporter.js'
22
+ import { getInstrumentations } from './pluggable-instrumentations.js'
23
+
24
+ const debuglog = util.debuglog('@platformatic/telemetry')
25
+ const require = createRequire(import.meta.url)
26
+
27
+ // See: https://www.npmjs.com/package/@opentelemetry/instrumentation-http
28
+ // When this is fixed we should set this to 'http' and fixe the tests
29
+ // https://github.com/open-telemetry/opentelemetry-js/issues/5103
30
+ process.env.OTEL_SEMCONV_STABILITY_OPT_IN = 'http/dup'
31
+
32
+ const setupNodeHTTPTelemetry = async (opts, applicationDir) => {
33
+ const { applicationName, instrumentations = [] } = opts
34
+ const additionalInstrumentations = await getInstrumentations(instrumentations, applicationDir)
35
+
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
+ }
76
+
77
+ const clientSpansAls = new AsyncLocalStorage()
78
+ globalThis.platformatic = globalThis.platformatic || {}
79
+ globalThis.platformatic.clientSpansAls = clientSpansAls
80
+
81
+ const sdk = new opentelemetry.NodeSDK({
82
+ spanProcessors, // https://github.com/open-telemetry/opentelemetry-js/issues/4881#issuecomment-2358059714
83
+ instrumentations: [
84
+ new UndiciInstrumentation({
85
+ responseHook: span => {
86
+ const store = clientSpansAls.getStore()
87
+ if (store) {
88
+ store.span = span
89
+ }
90
+ }
91
+ }),
92
+ new HttpInstrumentation(),
93
+ ...additionalInstrumentations
94
+ ],
95
+ resource: resourceFromAttributes({
96
+ [ATTR_SERVICE_NAME]: applicationName
97
+ })
98
+ })
99
+ sdk.start()
100
+
101
+ process.on('SIGTERM', () => {
102
+ sdk
103
+ .shutdown()
104
+ .then(() => debuglog('Tracing terminated'))
105
+ .catch(error => debuglog('Error terminating tracing', error))
106
+ })
107
+ }
108
+
109
+ async function main () {
110
+ let data = null
111
+ const useWorkerData = !!workerData
112
+
113
+ if (useWorkerData) {
114
+ data = workerData
115
+ } else if (process.env.PLT_MANAGER_ID) {
116
+ try {
117
+ const dataPath = resolve(tmpdir(), 'platformatic', 'runtimes', `${process.env.PLT_MANAGER_ID}.json`)
118
+ statSync(dataPath)
119
+ const jsonData = JSON.parse(readFileSync(dataPath, 'utf8'))
120
+ data = jsonData.data
121
+ debuglog(`Loaded data from ${dataPath}`)
122
+ } catch (e) {
123
+ debuglog('Error reading data from file %o', e)
124
+ }
125
+ }
126
+
127
+ if (data) {
128
+ debuglog('Setting up telemetry %o', data)
129
+ const applicationDir = data.applicationConfig?.path
130
+ const telemetryConfig = useWorkerData ? data?.applicationConfig?.telemetry : data?.telemetryConfig
131
+ if (telemetryConfig) {
132
+ debuglog('telemetryConfig %o', telemetryConfig)
133
+ setupNodeHTTPTelemetry(telemetryConfig, applicationDir)
134
+ }
135
+ }
136
+ }
137
+
138
+ try {
139
+ main()
140
+ } catch (e) {
141
+ debuglog('Error in main %o', e)
142
+ }
@@ -1,6 +1,4 @@
1
- 'use strict'
2
-
3
- const { createContextKey } = require('@opentelemetry/api')
1
+ import { createContextKey } from '@opentelemetry/api'
4
2
 
5
3
  // Unfortunately, these kesy are not exported by the OpenTelemetry API :()
6
4
  // And we HAVE to use these keys because are used by the propagators
@@ -9,13 +7,13 @@ const SPAN_KEY = createContextKey('OpenTelemetry Context Key SPAN')
9
7
  // This is basicaklly the same as https://github.com/open-telemetry/opentelemetry-js/blob/main/api/src/context/context.ts#L85
10
8
  // (so just a wrapper around a Map)
11
9
  // Note that mutating the context is not allowed by the OpenTelemetry spec.
12
- class PlatformaticContext {
10
+ export class PlatformaticContext {
13
11
  _currentContext
14
12
 
15
13
  constructor (parentContext) {
16
14
  this._currentContext = parentContext ? new Map(parentContext) : new Map()
17
15
 
18
- this.getValue = (key) => this._currentContext.get(key)
16
+ this.getValue = key => this._currentContext.get(key)
19
17
 
20
18
  // Must create and return a new context
21
19
  this.setValue = (key, value) => {
@@ -26,16 +24,14 @@ class PlatformaticContext {
26
24
 
27
25
  // Must return a new context
28
26
  /* istanbul ignore next */
29
- this.deleteValue = (key) => {
27
+ this.deleteValue = key => {
30
28
  const context = new PlatformaticContext(this._currentContext)
31
29
  context._currentContext.delete(key)
32
30
  return context
33
31
  }
34
32
 
35
- this.setSpan = (span) => {
33
+ this.setSpan = span => {
36
34
  return this.setValue(SPAN_KEY, span)
37
35
  }
38
36
  }
39
37
  }
40
-
41
- module.exports = { PlatformaticContext }
@@ -1,11 +1,14 @@
1
- 'use strict'
1
+ import { CompositePropagator, merge, W3CTraceContextPropagator } from '@opentelemetry/core'
2
+ import { emptyResource } from '@opentelemetry/resources'
3
+ import { AlwaysOnSampler } from '@opentelemetry/sdk-trace-base'
4
+ import { createRequire } from 'node:module'
5
+ import { MultiSpanProcessor } from './multispan-processor.js'
2
6
 
3
- const { Resource } = require('@opentelemetry/resources')
4
- const { AlwaysOnSampler, merge, CompositePropagator, W3CTraceContextPropagator } = require('@opentelemetry/core')
5
- const { Tracer } = require('@opentelemetry/sdk-trace-base')
6
- const { MultiSpanProcessor } = require('./multispan-processor')
7
+ const require = createRequire(import.meta.url)
8
+ // We need to import the Tracer to write our own TracerProvider that does NOT extend the OpenTelemetry one.
9
+ const { Tracer } = require('@opentelemetry/sdk-trace-base/build/src/Tracer')
7
10
 
8
- class PlatformaticTracerProvider {
11
+ export class PlatformaticTracerProvider {
9
12
  activeSpanProcessor = null
10
13
  _registeredSpanProcessors = []
11
14
  // This MUST be called `resource`, see: https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-sdk-trace-base/src/Tracer.ts#L57
@@ -16,19 +19,19 @@ class PlatformaticTracerProvider {
16
19
  const mergedConfig = merge(
17
20
  {},
18
21
  {
19
- sampler: new AlwaysOnSampler(),
22
+ sampler: new AlwaysOnSampler()
20
23
  },
21
24
  config
22
25
  )
23
- this.resource = mergedConfig.resource ?? Resource.empty()
26
+ this.resource = mergedConfig.resource ?? emptyResource
24
27
  this._config = Object.assign({}, mergedConfig, {
25
- resource: this.resource,
28
+ resource: this.resource
26
29
  })
27
30
  }
28
31
 
29
32
  // This is the only mandatory API: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#get-a-tracer
30
- getTracer (name, version, options) {
31
- return new Tracer({ name, version }, this._config, this)
33
+ getTracer (name, version) {
34
+ return new Tracer({ name, version }, this._config, this.resource, this.activeSpanProcessor)
32
35
  }
33
36
 
34
37
  addSpanProcessor (spanProcessor) {
@@ -37,9 +40,7 @@ class PlatformaticTracerProvider {
37
40
  } else {
38
41
  this._registeredSpanProcessors.push(spanProcessor)
39
42
  }
40
- this.activeSpanProcessor = new MultiSpanProcessor(
41
- this._registeredSpanProcessors
42
- )
43
+ this.activeSpanProcessor = new MultiSpanProcessor(this._registeredSpanProcessors)
43
44
  }
44
45
 
45
46
  getActiveSpanProcessor () {
@@ -49,14 +50,13 @@ class PlatformaticTracerProvider {
49
50
  getPropagator () {
50
51
  return new CompositePropagator({
51
52
  propagators: [
52
- new W3CTraceContextPropagator(), // see: https://www.w3.org/TR/trace-context/
53
- ],
53
+ new W3CTraceContextPropagator() // see: https://www.w3.org/TR/trace-context/
54
+ ]
54
55
  })
55
56
  }
56
57
 
57
58
  forceFlush () {
58
59
  // Let's do a fire-and-forget of forceFlush on all the processor for the time being.
59
- // TODO: manage errors
60
60
  this._registeredSpanProcessors.forEach(spanProcessor => spanProcessor.forceFlush())
61
61
  }
62
62
 
@@ -64,5 +64,3 @@ class PlatformaticTracerProvider {
64
64
  return this.activeSpanProcessor.shutdown()
65
65
  }
66
66
  }
67
-
68
- module.exports = { PlatformaticTracerProvider }
@@ -0,0 +1,62 @@
1
+ import { importOrLocal } from './import-or-local.js'
2
+
3
+ // These are already set automatically by the runtime, so we throw
4
+ // if set again.
5
+ const defaultInstrumentations = ['@opentelemetry/instrumentation-http', '@opentelemetry/instrumentation-undici']
6
+
7
+ async function getInstrumentationInstance (instrumentationConfig, applicationDir) {
8
+ if (typeof instrumentationConfig === 'string') {
9
+ instrumentationConfig = { package: instrumentationConfig, exportName: 'default', options: {} }
10
+ }
11
+ const { package: packageName, exportName = 'default', options = {} } = instrumentationConfig
12
+
13
+ if (defaultInstrumentations.includes(packageName)) {
14
+ throw new Error(
15
+ `Instrumentation package ${packageName} is already included by default, please remove it from your config.`
16
+ )
17
+ }
18
+
19
+ let mod
20
+ try {
21
+ mod = await importOrLocal({ pkg: packageName, projectDir: applicationDir })
22
+ } catch (err) {
23
+ throw new Error(
24
+ `Instrumentation package not found: ${instrumentationConfig.package}, please add it to your dependencies.`
25
+ )
26
+ }
27
+
28
+ let Instrumenter = mod[exportName]
29
+ if (!Instrumenter || typeof Instrumenter !== 'function') {
30
+ // Check for for an export that ends with 'Instrumentation'. We need to do that because unfortunately
31
+ // each instrumenttions has different named export. But all of them ends with 'Instrumentation'.
32
+ const possibleExports = Object.keys(mod).filter(key => key.endsWith('Instrumentation'))
33
+ if (possibleExports.length === 0) {
34
+ throw new Error(`Instrumentation export not found: ${exportName} in ${packageName}. Please specify in config`)
35
+ }
36
+ if (possibleExports.length > 1) {
37
+ throw new Error(
38
+ `Multiple Instrumentation exports found: ${possibleExports} in ${packageName}. Please specify in config`
39
+ )
40
+ }
41
+ Instrumenter = mod[possibleExports[0]]
42
+ }
43
+ const instance = new Instrumenter(options)
44
+ return instance
45
+ }
46
+
47
+ // Example of instrumentations config:
48
+ // "instrumentations": [
49
+ // "@opentelemetry/instrumentation-express",
50
+ // {
51
+ // "package": "@opentelemetry/instrumentation-redisjs",
52
+ // "exportName": "RedisInstrumentation",
53
+ // "options": { "foo": "bar" }
54
+ // }
55
+ export async function getInstrumentations (configs = [], applicationDir) {
56
+ const instrumentations = []
57
+ for (const instrumentationConfig of configs) {
58
+ const instance = await getInstrumentationInstance(instrumentationConfig, applicationDir)
59
+ instrumentations.push(instance)
60
+ }
61
+ return instrumentations
62
+ }
package/lib/schema.js CHANGED
@@ -1,73 +1,3 @@
1
- 'use strict'
1
+ import { schemaComponents } from '@platformatic/foundation'
2
2
 
3
- const ExporterSchema = {
4
- type: 'object',
5
- properties: {
6
- type: {
7
- type: 'string',
8
- enum: ['console', 'otlp', 'zipkin', 'memory'],
9
- default: 'console',
10
- },
11
- options: {
12
- type: 'object',
13
- description: 'Options for the exporter. These are passed directly to the exporter.',
14
- properties: {
15
- url: {
16
- type: 'string',
17
- description: 'The URL to send the traces to. Not used for console or memory exporters.',
18
- },
19
- headers: {
20
- type: 'object',
21
- description: 'Headers to send to the exporter. Not used for console or memory exporters.',
22
- },
23
- },
24
- },
25
- additionalProperties: false,
26
- },
27
- }
28
-
29
- const TelemetrySchema = {
30
- $id: '/OpenTelemetry',
31
- type: 'object',
32
- properties: {
33
- serviceName: {
34
- type: 'string',
35
- description: 'The name of the service. Defaults to the folder name if not specified.',
36
- },
37
- version: {
38
- type: 'string',
39
- description: 'The version of the service (optional)',
40
- },
41
- skip: {
42
- type: 'array',
43
- description: 'An array of paths to skip when creating spans. Useful for health checks and other endpoints that do not need to be traced.',
44
- items: {
45
- type: 'object',
46
- properties: {
47
- path: {
48
- type: 'string',
49
- description: 'The path to skip. Can be a string or a regex.',
50
- },
51
- method: {
52
- description: 'HTTP method to skip',
53
- type: 'string',
54
- enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'],
55
- },
56
- },
57
- },
58
- },
59
- exporter: {
60
- anyOf: [
61
- {
62
- type: 'array',
63
- items: ExporterSchema,
64
- },
65
- ExporterSchema,
66
- ],
67
- },
68
- },
69
- required: ['serviceName'],
70
- additionalProperties: false,
71
- }
72
-
73
- module.exports = TelemetrySchema
3
+ export default schemaComponents.telemetry