Skip to content

Commit bb3fea2

Browse files
committed
Add external OTel doc
Signed-off-by: Stephen Belanger <[email protected]>
1 parent 208e4a5 commit bb3fea2

File tree

4 files changed

+293
-1
lines changed

4 files changed

+293
-1
lines changed

docs/guides/distributed-tracing.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
Platformatic supports Open Telemetry integration. This allows you to send telemetry data to one of the OTLP compatible servers ([see here](https://opentelemetry.io/ecosystem/vendors/)) or to a Zipkin server. Let's show this with [Jaeger](https://www.jaegertracing.io/).
66

7+
:::tip Advanced Setup
8+
For manual OpenTelemetry SDK setup with custom instrumentations or exporters, see the [Advanced OpenTelemetry Setup](./opentelemetry-sdk-setup.md) guide.
9+
:::
10+
711
## Jaeger setup
812

913
The quickest way is to use docker:
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
# Advanced OpenTelemetry Setup with Watt
2+
3+
import Issues from '../getting-started/issues.md';
4+
5+
## Introduction
6+
7+
Watt includes [built-in telemetry support](./distributed-tracing.md) that can be configured declaratively in your `watt.json` or `platformatic.json` files. This works well for most use cases with OTLP and Zipkin exporters.
8+
9+
However, you may need manual OpenTelemetry SDK setup when you:
10+
11+
- Need custom instrumentations beyond what the built-in telemetry provides
12+
- Want to configure custom span processors or exporters
13+
- Need fine-grained control over OpenTelemetry SDK initialization
14+
15+
This guide covers how to set up the OpenTelemetry Node.js SDK manually in Watt.
16+
17+
## Understanding Multi-Worker Architecture
18+
19+
Watt runs each application in isolated [Node.js Worker Threads](../reference/runtime/multithread-architecture.md). This has important implications for OpenTelemetry setup:
20+
21+
- **Each worker is isolated**: Every worker thread runs its own OpenTelemetry SDK instance
22+
- **Initialization must happen early**: OpenTelemetry must load before any instrumented modules
23+
- **Context propagation is automatic**: Watt handles trace context propagation between workers via HTTP headers
24+
25+
The `execArgv` configuration with `--import` ensures your initialization script runs in each worker thread before application code loads.
26+
27+
## Configuration Options
28+
29+
Watt provides the `execArgv` configuration on each application to pass Node.js flags to worker threads. This is required for OpenTelemetry because the instrumentation hooks must be registered via `--import` before any application code loads.
30+
31+
### Application-Level Configuration
32+
33+
Use the `execArgv` option on each application to configure OpenTelemetry:
34+
35+
```json
36+
{
37+
"$schema": "https://schemas.platformatic.dev/wattpm/3.0.0.json",
38+
"applications": [
39+
{
40+
"id": "api",
41+
"path": "./services/api",
42+
"execArgv": [
43+
"--import", "@opentelemetry/instrumentation/hook.mjs",
44+
"--import", "./telemetry-init.mjs"
45+
]
46+
}
47+
],
48+
"server": {
49+
"port": 3000
50+
}
51+
}
52+
```
53+
54+
### Multiple Applications
55+
56+
When you have multiple applications, each needs its own `execArgv` configuration:
57+
58+
```json
59+
{
60+
"$schema": "https://schemas.platformatic.dev/wattpm/3.0.0.json",
61+
"applications": [
62+
{
63+
"id": "api",
64+
"path": "./services/api",
65+
"execArgv": [
66+
"--import", "@opentelemetry/instrumentation/hook.mjs",
67+
"--import", "./telemetry-init.mjs"
68+
]
69+
},
70+
{
71+
"id": "worker",
72+
"path": "./services/worker",
73+
"execArgv": [
74+
"--import", "@opentelemetry/instrumentation/hook.mjs",
75+
"--import", "./telemetry-init.mjs"
76+
]
77+
}
78+
]
79+
}
80+
```
81+
82+
## Initialization Script
83+
84+
The initialization script configures the OpenTelemetry SDK and must be loaded before any application code. Here's a complete example:
85+
86+
```javascript
87+
// telemetry-init.mjs
88+
import { workerData } from 'node:worker_threads'
89+
import { NodeSDK } from '@opentelemetry/sdk-node'
90+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
91+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
92+
import { Resource } from '@opentelemetry/resources'
93+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
94+
95+
// Get service name from workerData (set by Platformatic)
96+
const serviceName = workerData?.applicationConfig?.id || 'unknown-service'
97+
98+
const sdk = new NodeSDK({
99+
resource: new Resource({
100+
[ATTR_SERVICE_NAME]: serviceName,
101+
[ATTR_SERVICE_VERSION]: process.env.OTEL_SERVICE_VERSION || '1.0.0'
102+
}),
103+
traceExporter: new OTLPTraceExporter({
104+
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
105+
headers: process.env.OTEL_EXPORTER_OTLP_HEADERS
106+
? JSON.parse(process.env.OTEL_EXPORTER_OTLP_HEADERS)
107+
: {}
108+
}),
109+
instrumentations: [
110+
getNodeAutoInstrumentations({
111+
// Disable specific instrumentations if needed
112+
'@opentelemetry/instrumentation-fs': { enabled: false }
113+
})
114+
]
115+
})
116+
117+
sdk.start()
118+
119+
// Graceful shutdown to flush pending spans
120+
process.on('SIGTERM', () => {
121+
sdk.shutdown()
122+
.then(() => console.log('Telemetry terminated'))
123+
.catch((error) => console.log('Error terminating telemetry', error))
124+
.finally(() => process.exit(0))
125+
})
126+
```
127+
128+
The `workerData` object is automatically set by Watt for each worker thread and contains the application configuration. The `applicationConfig.id` property holds the service identifier as defined in your `watt.json`.
129+
130+
### Why the Hook is Required
131+
132+
The module loading order is critical:
133+
134+
1. Node.js processes `--import` flags in order before the application starts
135+
2. The OpenTelemetry hook registers loader hooks to intercept module imports
136+
3. Your initialization script configures and starts the SDK
137+
4. Application code loads (instrumentation is applied via the hook)
138+
139+
Without the hook, OpenTelemetry cannot intercept imports and instrumentation will not work.
140+
141+
### Disabling Built-in Telemetry
142+
143+
When using manual SDK setup, you should disable Watt's built-in telemetry to avoid conflicts (duplicate spans, multiple exporters, etc.):
144+
145+
```json
146+
{
147+
"applications": [
148+
{
149+
"id": "api",
150+
"path": "./services/api",
151+
"execArgv": [
152+
"--import", "@opentelemetry/instrumentation/hook.mjs",
153+
"--import", "./telemetry-init.mjs"
154+
],
155+
"telemetry": {
156+
"enabled": false
157+
}
158+
}
159+
]
160+
}
161+
```
162+
163+
## Troubleshooting
164+
165+
### Telemetry Not Appearing
166+
167+
1. **Check module loading order**: Ensure OpenTelemetry loads before application code
168+
2. **Verify exporter URL**: Confirm the collector endpoint is accessible
169+
3. **Check for errors**: Look for initialization errors in logs
170+
4. **Validate configuration**: Ensure environment variables are set correctly
171+
172+
### Module Loading Errors
173+
174+
Common issues:
175+
176+
- **"Cannot use import statement outside a module"**: Ensure your initialization file has `.mjs` extension
177+
- **Module not found**: Check the import path is correct and the package is installed
178+
179+
### OpenTelemetry SDK Not Initializing
180+
181+
1. Verify all required environment variables are set
182+
2. Ensure the module path in `execArgv` is correct and the module exists
183+
3. Check for initialization errors in the console output
184+
4. Verify the OTLP endpoint is accessible from the application
185+
186+
## Complete Example with Jaeger
187+
188+
**watt.json:**
189+
```json
190+
{
191+
"$schema": "https://schemas.platformatic.dev/wattpm/3.0.0.json",
192+
"entrypoint": "api",
193+
"applications": [
194+
{
195+
"id": "api",
196+
"path": "./services/api",
197+
"execArgv": [
198+
"--import", "@opentelemetry/instrumentation/hook.mjs",
199+
"--import", "./telemetry.mjs"
200+
]
201+
}
202+
],
203+
"env": {
204+
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318/v1/traces"
205+
},
206+
"server": {
207+
"port": 3000
208+
}
209+
}
210+
```
211+
212+
**telemetry.mjs:**
213+
```javascript
214+
import { workerData } from 'node:worker_threads'
215+
import { NodeSDK } from '@opentelemetry/sdk-node'
216+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
217+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
218+
import { Resource } from '@opentelemetry/resources'
219+
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
220+
221+
// Get service name from workerData (set by Platformatic)
222+
const serviceName = workerData?.applicationConfig?.id || 'unknown-service'
223+
224+
const sdk = new NodeSDK({
225+
resource: new Resource({
226+
[ATTR_SERVICE_NAME]: serviceName
227+
}),
228+
traceExporter: new OTLPTraceExporter({
229+
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
230+
}),
231+
instrumentations: [getNodeAutoInstrumentations()]
232+
})
233+
234+
sdk.start()
235+
236+
process.on('SIGTERM', () => sdk.shutdown())
237+
```
238+
239+
**Start Jaeger:**
240+
```bash
241+
docker run -d --name jaeger \
242+
-e COLLECTOR_OTLP_ENABLED=true \
243+
-p 16686:16686 \
244+
-p 4317:4317 \
245+
-p 4318:4318 \
246+
jaegertracing/all-in-one:latest
247+
```
248+
249+
<Issues />

0 commit comments

Comments
 (0)