@platformatic/telemetry 0.34.0 → 0.35.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/lib/schema.js CHANGED
@@ -12,6 +12,13 @@ const TelemetrySchema = {
12
12
  type: 'string',
13
13
  description: 'The version of the service (optional)'
14
14
  },
15
+ skip: {
16
+ type: 'array',
17
+ description: 'An array of paths to skip when creating spans. Useful for health checks and other endpoints that do not need to be traced.',
18
+ items: {
19
+ type: 'string'
20
+ }
21
+ },
15
22
  exporter: {
16
23
  type: 'object',
17
24
  properties: {
package/lib/telemetry.js CHANGED
@@ -1,13 +1,14 @@
1
1
  'use strict'
2
2
 
3
3
  const fp = require('fastify-plugin')
4
- const { SpanStatusCode } = require('@opentelemetry/api')
4
+ const { SpanStatusCode, SpanKind } = require('@opentelemetry/api')
5
5
  const { ConsoleSpanExporter, BatchSpanProcessor, SimpleSpanProcessor, InMemorySpanExporter } = require('@opentelemetry/sdk-trace-base')
6
6
  const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions')
7
7
  const { Resource } = require('@opentelemetry/resources')
8
8
  const { PlatformaticTracerProvider } = require('./platformatic-trace-provider')
9
9
  const { PlatformaticContext } = require('./platformatic-context')
10
10
  const { fastifyTextMapGetter, fastifyTextMapSetter } = require('./fastify-text-map')
11
+ const fastUri = require('fast-uri')
11
12
 
12
13
  // Platformatic telemetry plugin.
13
14
  // Supported Exporters:
@@ -32,16 +33,22 @@ function formatSpanName (request) {
32
33
  return routerPath ? `${method} ${routerPath}` : method
33
34
  }
34
35
 
35
- const defaultFormatSpanAttributes = {
36
+ const formatSpanAttributes = {
36
37
  request (request) {
38
+ const { hostname, method, url, protocol } = request
39
+ // Inspired by: https://github.com/fastify/fastify-url-data/blob/master/plugin.js#L11
40
+ const urlData = fastUri.parse(`${protocol}://${hostname}${url}`)
37
41
  return {
38
- 'req.method': request.raw.method,
39
- 'req.url': request.raw.url
42
+ 'server.address': hostname,
43
+ 'server.port': urlData.port,
44
+ 'http.request.method': method,
45
+ 'url.path': urlData.path,
46
+ 'url.scheme': protocol
40
47
  }
41
48
  },
42
49
  reply (reply) {
43
50
  return {
44
- 'reply.statusCode': reply.statusCode
51
+ 'http.response.status_code': reply.statusCode
45
52
  }
46
53
  },
47
54
  error (error) {
@@ -60,7 +67,7 @@ const setupProvider = (app, opts) => {
60
67
  app.log.warn('No exporter configured, defaulting to console.')
61
68
  exporter = { type: 'console' }
62
69
  }
63
- app.log.info(`Setting up telemetry for service: ${serviceName} version: ${version} and exporter of type ${exporter.type}`)
70
+ app.log.info(`Setting up telemetry for service: ${serviceName}${version ? ' version: ' + version : ''} with exporter of type ${exporter.type}`)
64
71
  const provider = new PlatformaticTracerProvider({
65
72
  resource: new Resource({
66
73
  [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
@@ -101,16 +108,17 @@ async function setupTelemetry (app, opts) {
101
108
  // const { serviceName, version } = opts
102
109
  const openTelemetryAPIs = setupProvider(app, opts)
103
110
  const { tracer, propagator, provider } = openTelemetryAPIs
104
-
105
- const formatSpanAttributes = {
106
- ...defaultFormatSpanAttributes,
107
- ...(opts.formatSpanAttributes || {})
108
- }
111
+ const skipOperations = opts.skip || []
109
112
 
110
113
  // expose the span as a request decorator
111
114
  app.decorateRequest('span')
112
115
 
113
116
  const startSpan = async (request) => {
117
+ if (skipOperations.includes(`${request.method}${request.url}`)) {
118
+ request.log.debug({ operation: `${request.method}${request.url}` }, 'Skipping telemetry')
119
+ return
120
+ }
121
+
114
122
  // We populate the context with the incoming request headers
115
123
  let context = propagator.extract(new PlatformaticContext(), request, fastifyTextMapGetter)
116
124
 
@@ -119,6 +127,7 @@ async function setupTelemetry (app, opts) {
119
127
  {},
120
128
  context
121
129
  )
130
+ span.kind = SpanKind.SERVER
122
131
  // Next 2 lines are needed by W3CTraceContextPropagator
123
132
  context = context.setSpan(span)
124
133
  span.setAttributes(formatSpanAttributes.request(request))
@@ -132,19 +141,23 @@ async function setupTelemetry (app, opts) {
132
141
  }
133
142
 
134
143
  const injectPropagationHeadersInReply = async (request, reply) => {
135
- const context = request.span.context
136
- propagator.inject(context, reply, fastifyTextMapSetter)
144
+ if (request.span) {
145
+ const context = request.span.context
146
+ propagator.inject(context, reply, fastifyTextMapSetter)
147
+ }
137
148
  }
138
149
 
139
150
  const endSpan = async (request, reply) => {
140
151
  const span = request.span
141
- const spanStatus = { code: SpanStatusCode.OK }
142
- if (reply.statusCode >= 400) {
143
- spanStatus.code = SpanStatusCode.ERROR
152
+ if (span) {
153
+ const spanStatus = { code: SpanStatusCode.OK }
154
+ if (reply.statusCode >= 400) {
155
+ spanStatus.code = SpanStatusCode.ERROR
156
+ }
157
+ span.setAttributes(formatSpanAttributes.reply(reply))
158
+ span.setStatus(spanStatus)
159
+ span.end()
144
160
  }
145
- span.setAttributes(formatSpanAttributes.reply(reply))
146
- span.setStatus(spanStatus)
147
- span.end()
148
161
  }
149
162
 
150
163
  app.addHook('onRequest', startSpan)
@@ -174,11 +187,30 @@ async function setupTelemetry (app, opts) {
174
187
  // - closing the span
175
188
  const startSpanClient = (url, method, ctx) => {
176
189
  let context = ctx || new PlatformaticContext()
190
+ const urlObj = fastUri.parse(url)
191
+
192
+ if (skipOperations.includes(`${method}${urlObj.path}`)) {
193
+ app.log.debug({ operation: `${method}${urlObj.path}` }, 'Skipping telemetry')
194
+ return
195
+ }
196
+
177
197
  /* istanbul ignore next */
178
198
  method = method || ''
179
- const span = tracer.startSpan(`${method} ${url}`, {}, context)
199
+ const name = `${method} ${urlObj.scheme}://${urlObj.host}:${urlObj.port}${urlObj.path}`
200
+
201
+ const span = tracer.startSpan(name, {}, context)
202
+ span.kind = SpanKind.CLIENT
203
+
180
204
  /* istanbul ignore next */
181
- const attributes = url ? { 'server.url': url } : {}
205
+ const attributes = url
206
+ ? {
207
+ 'server.address': urlObj.host,
208
+ 'server.port': urlObj.port,
209
+ 'http.request.method': method,
210
+ 'url.full': url,
211
+ 'url.path': urlObj.path
212
+ }
213
+ : {}
182
214
  span.setAttributes(attributes)
183
215
 
184
216
  // Next 2 lines are needed by W3CTraceContextPropagator
@@ -200,7 +232,7 @@ async function setupTelemetry (app, opts) {
200
232
  spanStatus.code = SpanStatusCode.ERROR
201
233
  }
202
234
  span.setAttributes({
203
- 'response.statusCode': response.statusCode
235
+ 'http.response.status_code': response.statusCode
204
236
  })
205
237
  span.setStatus(spanStatus)
206
238
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/telemetry",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "description": "OpenTelemetry integration for Platformatic",
5
5
  "main": "index.js",
6
6
  "author": "Marco Piraccini <marco.piraccini@gmail.com>",
@@ -23,6 +23,7 @@
23
23
  "@opentelemetry/resources": "^1.15.0",
24
24
  "@opentelemetry/sdk-trace-base": "^1.15.0",
25
25
  "@opentelemetry/semantic-conventions": "^1.15.0",
26
+ "fast-uri": "^2.2.0",
26
27
  "fastify-plugin": "^4.5.0"
27
28
  },
28
29
  "scripts": {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { test } = require('tap')
4
4
  const fastify = require('fastify')
5
- const { SpanStatusCode } = require('@opentelemetry/api')
5
+ const { SpanStatusCode, SpanKind } = require('@opentelemetry/api')
6
6
  const telemetryPlugin = require('../lib/telemetry')
7
7
  const { PlatformaticContext } = require('../lib/platformatic-context')
8
8
  const { fastifyTextMapGetter } = require('../lib/fastify-text-map')
@@ -29,7 +29,6 @@ test('should add the propagation headers correctly, new propagation started', as
29
29
 
30
30
  const app = await setupApp({
31
31
  serviceName: 'test-service',
32
- version: '1.0.0',
33
32
  exporter: {
34
33
  type: 'memory'
35
34
  }
@@ -126,16 +125,21 @@ test('should trace a client request', async ({ equal, same, teardown }) => {
126
125
  // We have two one for the client and one for the server
127
126
  const spanServer = finishedSpans[0]
128
127
  equal(spanServer.name, 'GET /test')
128
+ equal(spanServer.kind, SpanKind.SERVER)
129
129
  equal(spanServer.status.code, SpanStatusCode.OK)
130
- equal(spanServer.attributes['req.method'], 'GET')
131
- equal(spanServer.attributes['req.url'], '/test')
132
- equal(spanServer.attributes['reply.statusCode'], 200)
130
+ equal(spanServer.attributes['http.request.method'], 'GET')
131
+ equal(spanServer.attributes['url.path'], '/test')
132
+ equal(spanServer.attributes['http.response.status_code'], 200)
133
133
 
134
134
  const spanClient = finishedSpans[1]
135
135
  equal(spanClient.name, 'GET http://localhost:3000/test')
136
+ equal(spanClient.kind, SpanKind.CLIENT)
136
137
  equal(spanClient.status.code, SpanStatusCode.OK)
137
- equal(spanClient.attributes['server.url'], 'http://localhost:3000/test')
138
- equal(spanClient.attributes['response.statusCode'], 200)
138
+ equal(spanClient.attributes['url.full'], 'http://localhost:3000/test')
139
+ equal(spanClient.attributes['http.response.status_code'], 200)
140
+ equal(spanClient.attributes['server.port'], 3000)
141
+ equal(spanClient.attributes['server.address'], 'localhost')
142
+ equal(spanClient.attributes['url.path'], '/test')
139
143
 
140
144
  // The traceparent header is added to the request and propagated to the server
141
145
  equal(receivedHeaders.traceparent, telemetryHeaders.traceparent)
@@ -175,16 +179,18 @@ test('should trace a client request failing', async ({ equal, same, teardown })
175
179
  // We have two one for the client and one for the server
176
180
  const spanServer = finishedSpans[0]
177
181
  equal(spanServer.name, 'GET')
182
+ equal(spanServer.kind, SpanKind.SERVER)
178
183
  equal(spanServer.status.code, SpanStatusCode.ERROR)
179
- equal(spanServer.attributes['req.method'], 'GET')
180
- equal(spanServer.attributes['req.url'], '/wrong')
181
- equal(spanServer.attributes['reply.statusCode'], 404)
184
+ equal(spanServer.attributes['http.request.method'], 'GET')
185
+ equal(spanServer.attributes['url.path'], '/wrong')
186
+ equal(spanServer.attributes['http.response.status_code'], 404)
182
187
 
183
188
  const spanClient = finishedSpans[1]
184
189
  equal(spanClient.name, 'GET http://localhost:3000/test')
190
+ equal(spanClient.kind, SpanKind.CLIENT)
185
191
  equal(spanClient.status.code, SpanStatusCode.ERROR)
186
- equal(spanClient.attributes['server.url'], 'http://localhost:3000/test')
187
- equal(spanClient.attributes['response.statusCode'], 404)
192
+ equal(spanClient.attributes['url.full'], 'http://localhost:3000/test')
193
+ equal(spanClient.attributes['http.response.status_code'], 404)
188
194
  })
189
195
 
190
196
  test('should trace a client request failing (no HTTP error)', async ({ equal, same, teardown }) => {
@@ -219,8 +225,48 @@ test('should trace a client request failing (no HTTP error)', async ({ equal, sa
219
225
  const spanClient = finishedSpans[0]
220
226
  equal(spanClient.name, 'GET http://localhost:3000/test')
221
227
  equal(spanClient.status.code, SpanStatusCode.ERROR)
222
- equal(spanClient.attributes['server.url'], 'http://localhost:3000/test')
228
+ equal(spanClient.attributes['url.full'], 'http://localhost:3000/test')
223
229
  equal(spanClient.attributes['error.name'], 'Error')
224
230
  equal(spanClient.attributes['error.message'], 'KABOOM!!!')
225
231
  equal(spanClient.attributes['error.stack'].includes('Error: KABOOM!!!'), true)
226
232
  })
233
+
234
+ test('should not add the query in span name', async ({ equal, same, teardown }) => {
235
+ const handler = async (request, reply) => {
236
+ return { foo: 'bar' }
237
+ }
238
+
239
+ const app = await setupApp({
240
+ serviceName: 'test-service',
241
+ exporter: {
242
+ type: 'memory'
243
+ }
244
+ }, handler, teardown)
245
+
246
+ const { startSpanClient } = app.openTelemetry
247
+
248
+ const url = 'http://localhost:3000/test?foo=bar'
249
+ const { span } = startSpanClient(url, 'GET')
250
+ same(span.name, 'GET http://localhost:3000/test')
251
+ })
252
+
253
+ test('should ignore the skipped operations', async ({ equal, same, ok, teardown }) => {
254
+ const handler = async (request, reply) => {
255
+ return { foo: 'bar' }
256
+ }
257
+
258
+ const app = await setupApp({
259
+ serviceName: 'test-service',
260
+ skip: ['POST/skipme'],
261
+ exporter: {
262
+ type: 'memory'
263
+ }
264
+ }, handler, teardown)
265
+
266
+ const { startSpanClient } = app.openTelemetry
267
+
268
+ const url = 'http://localhost:3000/skipme'
269
+ const ret = startSpanClient(url, 'POST')
270
+ // no spam should be created
271
+ ok(!ret)
272
+ })
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { test } = require('tap')
4
4
  const fastify = require('fastify')
5
- const { SpanStatusCode } = require('@opentelemetry/api')
5
+ const { SpanStatusCode, SpanKind } = require('@opentelemetry/api')
6
6
  const telemetryPlugin = require('../lib/telemetry')
7
7
 
8
8
  async function setupApp (pluginOpts, routeHandler, teardown) {
@@ -46,11 +46,53 @@ test('should trace a request not failing', async ({ equal, same, teardown }) =>
46
46
  const finishedSpans = exporter.getFinishedSpans()
47
47
  equal(finishedSpans.length, 1)
48
48
  const span = finishedSpans[0]
49
+ equal(span.kind, SpanKind.SERVER)
49
50
  equal(span.name, 'GET /test')
50
51
  equal(span.status.code, SpanStatusCode.OK)
51
- equal(span.attributes['req.method'], 'GET')
52
- equal(span.attributes['req.url'], '/test')
53
- equal(span.attributes['reply.statusCode'], 200)
52
+ equal(span.attributes['http.request.method'], 'GET')
53
+ equal(span.attributes['url.path'], '/test')
54
+ equal(span.attributes['http.response.status_code'], 200)
55
+ equal(span.attributes['url.scheme'], 'http')
56
+ equal(span.attributes['server.address'], 'test')
57
+ const resource = span.resource
58
+ same(resource.attributes['service.name'], 'test-service')
59
+ same(resource.attributes['service.version'], '1.0.0')
60
+ })
61
+
62
+ test('should not put query in `url.path', async ({ equal, same, teardown }) => {
63
+ const handler = async (request, reply) => {
64
+ return { foo: 'bar' }
65
+ }
66
+
67
+ const injectArgs = {
68
+ method: 'GET',
69
+ url: '/test?foo=bar',
70
+ headers: {
71
+ host: 'test'
72
+ }
73
+ }
74
+
75
+ const app = await setupApp({
76
+ serviceName: 'test-service',
77
+ version: '1.0.0',
78
+ exporter: {
79
+ type: 'memory'
80
+ }
81
+ }, handler, teardown)
82
+
83
+ await app.inject(injectArgs)
84
+ const { exporter } = app.openTelemetry
85
+ const finishedSpans = exporter.getFinishedSpans()
86
+ equal(finishedSpans.length, 1)
87
+ const span = finishedSpans[0]
88
+ equal(span.kind, SpanKind.SERVER)
89
+ equal(span.name, 'GET /test')
90
+ equal(span.status.code, SpanStatusCode.OK)
91
+ equal(span.attributes['http.request.method'], 'GET')
92
+ equal(span.attributes['url.path'], '/test')
93
+ equal(span.attributes['http.response.status_code'], 200)
94
+ equal(span.attributes['url.scheme'], 'http')
95
+ equal(span.attributes['server.address'], 'test')
54
96
  const resource = span.resource
55
97
  same(resource.attributes['service.name'], 'test-service')
56
98
  same(resource.attributes['service.version'], '1.0.0')
@@ -77,9 +119,9 @@ test('request should add attribute to a span', async ({ equal, same, teardown })
77
119
  const span = finishedSpans[0]
78
120
  equal(span.name, 'GET /test')
79
121
  equal(span.status.code, SpanStatusCode.OK)
80
- equal(span.attributes['req.method'], 'GET')
81
- equal(span.attributes['req.url'], '/test')
82
- equal(span.attributes['reply.statusCode'], 200)
122
+ equal(span.attributes['http.request.method'], 'GET')
123
+ equal(span.attributes['url.path'], '/test')
124
+ equal(span.attributes['http.response.status_code'], 200)
83
125
  // This is the attribute we added
84
126
  equal(span.attributes.foo, 'bar')
85
127
  const resource = span.resource
@@ -127,9 +169,9 @@ test('should trace a request that fails', async ({ equal, same, teardown }) => {
127
169
  const span = finishedSpans[0]
128
170
  equal(span.name, 'GET /test')
129
171
  equal(span.status.code, SpanStatusCode.ERROR)
130
- equal(span.attributes['req.method'], 'GET')
131
- equal(span.attributes['req.url'], '/test')
132
- equal(span.attributes['reply.statusCode'], 500)
172
+ equal(span.attributes['http.request.method'], 'GET')
173
+ equal(span.attributes['url.path'], '/test')
174
+ equal(span.attributes['http.response.status_code'], 500)
133
175
  equal(span.attributes['error.message'], 'booooom!!!')
134
176
  const resource = span.resource
135
177
  same(resource.attributes['service.name'], 'test-service')
@@ -206,3 +248,33 @@ test('wrong exporter is configured, should default to console', async ({ equal,
206
248
  const { exporter } = app.openTelemetry
207
249
  same(exporter.constructor.name, 'ConsoleSpanExporter')
208
250
  })
251
+
252
+ test('should not trace if the operation is skipped', async ({ equal, same, teardown }) => {
253
+ const handler = async (request, reply) => {
254
+ return { foo: 'bar' }
255
+ }
256
+
257
+ const app = await setupApp({
258
+ serviceName: 'test-service',
259
+ version: '1.0.0',
260
+ skip: [
261
+ 'GET/documentation/json'
262
+ ],
263
+ exporter: {
264
+ type: 'memory'
265
+ }
266
+ }, handler, teardown)
267
+
268
+ const injectArgs = {
269
+ method: 'GET',
270
+ url: '/documentation/json',
271
+ headers: {
272
+ host: 'test'
273
+ }
274
+ }
275
+
276
+ await app.inject(injectArgs)
277
+ const { exporter } = app.openTelemetry
278
+ const finishedSpans = exporter.getFinishedSpans()
279
+ equal(finishedSpans.length, 0)
280
+ })