@platformatic/metrics 3.13.1 → 3.15.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/index.js CHANGED
@@ -189,6 +189,58 @@ export async function collectMetrics (applicationId, workerId, metricsConfig = {
189
189
  }
190
190
 
191
191
  return {
192
- registry
192
+ registry,
193
+ otlpBridge: null
193
194
  }
194
195
  }
196
+
197
+ export async function setupOtlpExporter (registry, otlpExporterConfig, applicationId) {
198
+ if (!otlpExporterConfig || !otlpExporterConfig.endpoint) {
199
+ return null
200
+ }
201
+
202
+ // Check if explicitly disabled
203
+ if (otlpExporterConfig.enabled === false || otlpExporterConfig.enabled === 'false') {
204
+ return null
205
+ }
206
+
207
+ // Dynamically import PromClientBridge to defer loading until after telemetry is initialized
208
+ const { PromClientBridge } = await import('@platformatic/promotel')
209
+
210
+ const {
211
+ endpoint,
212
+ headers,
213
+ interval = 60000,
214
+ serviceName = applicationId,
215
+ serviceVersion
216
+ } = otlpExporterConfig
217
+
218
+ const otlpEndpointOptions = {
219
+ url: endpoint
220
+ }
221
+
222
+ if (headers) {
223
+ otlpEndpointOptions.headers = headers
224
+ }
225
+
226
+ const conversionOptions = {
227
+ serviceName
228
+ }
229
+
230
+ if (serviceVersion) {
231
+ conversionOptions.serviceVersion = serviceVersion
232
+ }
233
+
234
+ const bridge = new PromClientBridge({
235
+ registry,
236
+ otlpEndpoint: otlpEndpointOptions,
237
+ interval,
238
+ conversionOptions,
239
+ onError: (error) => {
240
+ // Log error but don't crash the application
241
+ console.error('OTLP metrics export error:', error)
242
+ }
243
+ })
244
+
245
+ return bridge
246
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/metrics",
3
- "version": "3.13.1",
3
+ "version": "3.15.0",
4
4
  "description": "Platformatic Capability Metrics",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -16,6 +16,7 @@
16
16
  "homepage": "https://github.com/platformatic/platformatic#readme",
17
17
  "dependencies": {
18
18
  "@platformatic/http-metrics": "^0.2.1",
19
+ "@platformatic/promotel": "^0.1.0",
19
20
  "prom-client": "^15.1.2"
20
21
  },
21
22
  "devDependencies": {
@@ -0,0 +1,141 @@
1
+ import assert from 'node:assert'
2
+ import { test } from 'node:test'
3
+ import { client, collectMetrics, setupOtlpExporter } from '../index.js'
4
+
5
+ test('setupOtlpExporter returns null when no config provided', async () => {
6
+ const registry = new client.Registry()
7
+ const bridge = await setupOtlpExporter(registry, null, 'test-app')
8
+ assert.strictEqual(bridge, null)
9
+ })
10
+
11
+ test('setupOtlpExporter returns null when no endpoint provided', async () => {
12
+ const registry = new client.Registry()
13
+ const bridge = await setupOtlpExporter(registry, {}, 'test-app')
14
+ assert.strictEqual(bridge, null)
15
+ })
16
+
17
+ test('setupOtlpExporter returns null when explicitly disabled', async () => {
18
+ const registry = new client.Registry()
19
+ const bridge = await setupOtlpExporter(registry, {
20
+ enabled: false,
21
+ endpoint: 'http://localhost:4318/v1/metrics'
22
+ }, 'test-app')
23
+ assert.strictEqual(bridge, null)
24
+ })
25
+
26
+ test('setupOtlpExporter returns null when disabled via string', async () => {
27
+ const registry = new client.Registry()
28
+ const bridge = await setupOtlpExporter(registry, {
29
+ enabled: 'false',
30
+ endpoint: 'http://localhost:4318/v1/metrics'
31
+ }, 'test-app')
32
+ assert.strictEqual(bridge, null)
33
+ })
34
+
35
+ test('setupOtlpExporter creates bridge with minimal config', async () => {
36
+ const registry = new client.Registry()
37
+ const bridge = await setupOtlpExporter(registry, {
38
+ endpoint: 'http://localhost:4318/v1/metrics'
39
+ }, 'test-app')
40
+
41
+ assert.ok(bridge, 'Bridge should be created')
42
+ assert.strictEqual(bridge.running, false, 'Bridge should not be running initially')
43
+
44
+ const config = bridge.config
45
+ assert.strictEqual(config.otlpEndpoint.url, 'http://localhost:4318/v1/metrics')
46
+ assert.strictEqual(config.interval, 60000, 'Default interval should be 60000')
47
+ assert.strictEqual(config.conversionOptions.serviceName, 'test-app')
48
+ })
49
+
50
+ test('setupOtlpExporter creates bridge with full config', async () => {
51
+ const registry = new client.Registry()
52
+ const bridge = await setupOtlpExporter(registry, {
53
+ endpoint: 'http://collector:4318/v1/metrics',
54
+ headers: {
55
+ 'x-api-key': 'secret123',
56
+ 'x-custom-header': 'value'
57
+ },
58
+ interval: 30000,
59
+ serviceName: 'my-service',
60
+ serviceVersion: '1.2.3'
61
+ }, 'test-app')
62
+
63
+ assert.ok(bridge, 'Bridge should be created')
64
+
65
+ const config = bridge.config
66
+ assert.strictEqual(config.otlpEndpoint.url, 'http://collector:4318/v1/metrics')
67
+ assert.deepStrictEqual(config.otlpEndpoint.headers, {
68
+ 'x-api-key': 'secret123',
69
+ 'x-custom-header': 'value'
70
+ })
71
+ assert.strictEqual(config.interval, 30000)
72
+ assert.strictEqual(config.conversionOptions.serviceName, 'my-service')
73
+ assert.strictEqual(config.conversionOptions.serviceVersion, '1.2.3')
74
+ })
75
+
76
+ test('setupOtlpExporter uses applicationId as default serviceName', async () => {
77
+ const registry = new client.Registry()
78
+ const bridge = await setupOtlpExporter(registry, {
79
+ endpoint: 'http://localhost:4318/v1/metrics'
80
+ }, 'my-app-id')
81
+
82
+ assert.ok(bridge, 'Bridge should be created')
83
+ assert.strictEqual(bridge.config.conversionOptions.serviceName, 'my-app-id')
84
+ })
85
+
86
+ test('collectMetrics returns registry and null otlpBridge', async () => {
87
+ const result = await collectMetrics('test-service', 1, {
88
+ defaultMetrics: false,
89
+ httpMetrics: false
90
+ })
91
+
92
+ assert.ok(result.registry)
93
+ assert.strictEqual(result.otlpBridge, null)
94
+ })
95
+
96
+ test('bridge lifecycle - start and stop', async () => {
97
+ const registry = new client.Registry()
98
+ const bridge = await setupOtlpExporter(registry, {
99
+ endpoint: 'http://localhost:4318/v1/metrics',
100
+ interval: 60000
101
+ }, 'test-app')
102
+
103
+ assert.ok(bridge, 'Bridge should be created')
104
+ assert.strictEqual(bridge.running, false, 'Should not be running initially')
105
+
106
+ // Start bridge
107
+ bridge.start()
108
+ assert.strictEqual(bridge.running, true, 'Should be running after start')
109
+
110
+ // Stop bridge
111
+ bridge.stop()
112
+ assert.strictEqual(bridge.running, false, 'Should not be running after stop')
113
+ })
114
+
115
+ test('bridge handles errors gracefully', async () => {
116
+ const registry = new client.Registry()
117
+
118
+ // Add a test metric
119
+ const counter = new client.Counter({
120
+ name: 'test_counter',
121
+ help: 'Test counter',
122
+ registers: [registry]
123
+ })
124
+ counter.inc(5)
125
+
126
+ const bridge = await setupOtlpExporter(registry, {
127
+ endpoint: 'http://invalid-endpoint-that-does-not-exist:9999/v1/metrics',
128
+ interval: 1000 // Short interval for testing
129
+ }, 'test-app')
130
+
131
+ assert.ok(bridge, 'Bridge should be created')
132
+
133
+ // The bridge should not crash the app even with invalid endpoint
134
+ // Errors are logged but don't throw
135
+ bridge.start()
136
+ assert.strictEqual(bridge.running, true)
137
+
138
+ // Clean up
139
+ bridge.stop()
140
+ assert.strictEqual(bridge.running, false)
141
+ })