@infinitedusky/indusk-mcp 1.5.8 → 1.5.9
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/dist/bin/commands/init.js +61 -1
- package/extensions/otel/manifest.json +23 -0
- package/extensions/otel/skill.md +153 -0
- package/package.json +1 -1
- package/templates/__pycache__/instrumentation.cpython-314.pyc +0 -0
- package/templates/filtering-exporter.ts +82 -0
- package/templates/instrumentation.py +65 -0
- package/templates/instrumentation.ts +63 -0
- package/templates/logger.ts +51 -0
|
@@ -175,7 +175,67 @@ export async function init(projectRoot, options = {}) {
|
|
|
175
175
|
cpSync(join(packageRoot, "templates/biome.template.json"), biomePath);
|
|
176
176
|
console.info(` ${existsSync(biomePath) ? "overwrite" : "create"}: biome.json`);
|
|
177
177
|
}
|
|
178
|
-
// 7.
|
|
178
|
+
// 7. Scaffold OpenTelemetry instrumentation
|
|
179
|
+
console.info("\n[OpenTelemetry]");
|
|
180
|
+
const otelTemplates = ["instrumentation.ts", "filtering-exporter.ts", "logger.ts"];
|
|
181
|
+
const isNextJs = existsSync(join(projectRoot, "next.config.js")) ||
|
|
182
|
+
existsSync(join(projectRoot, "next.config.ts")) ||
|
|
183
|
+
existsSync(join(projectRoot, "next.config.mjs"));
|
|
184
|
+
const isPython = existsSync(join(projectRoot, "requirements.txt")) ||
|
|
185
|
+
existsSync(join(projectRoot, "pyproject.toml"));
|
|
186
|
+
if (isPython) {
|
|
187
|
+
const pyTemplate = "instrumentation.py";
|
|
188
|
+
const targetFile = join(projectRoot, pyTemplate);
|
|
189
|
+
if (existsSync(targetFile) && !force) {
|
|
190
|
+
console.info(` skip: ${pyTemplate} (already exists)`);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
cpSync(join(packageRoot, "templates", pyTemplate), targetFile);
|
|
194
|
+
console.info(` create: ${pyTemplate}`);
|
|
195
|
+
console.info(" install: pip install opentelemetry-distro opentelemetry-instrumentation opentelemetry-exporter-otlp");
|
|
196
|
+
console.info(" run: opentelemetry-instrument python your_app.py");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// Node.js or Next.js — copy TypeScript templates
|
|
201
|
+
const targetDir = isNextJs ? projectRoot : join(projectRoot, "src");
|
|
202
|
+
const srcDir = existsSync(join(projectRoot, "src")) ? join(projectRoot, "src") : projectRoot;
|
|
203
|
+
for (const template of otelTemplates) {
|
|
204
|
+
const targetFile = join(srcDir, template);
|
|
205
|
+
if (existsSync(targetFile) && !force) {
|
|
206
|
+
console.info(` skip: ${template} (already exists)`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
cpSync(join(packageRoot, "templates", template), targetFile);
|
|
210
|
+
console.info(` create: ${template}`);
|
|
211
|
+
}
|
|
212
|
+
// Set service name in instrumentation.ts
|
|
213
|
+
const instrPath = join(srcDir, "instrumentation.ts");
|
|
214
|
+
if (existsSync(instrPath)) {
|
|
215
|
+
const content = readFileSync(instrPath, "utf-8");
|
|
216
|
+
writeFileSync(instrPath, content.replace('"unknown-service"', `"${projectName}"`));
|
|
217
|
+
}
|
|
218
|
+
// Print instructions
|
|
219
|
+
const packages = [
|
|
220
|
+
"@opentelemetry/sdk-node",
|
|
221
|
+
"@opentelemetry/auto-instrumentations-node",
|
|
222
|
+
"@opentelemetry/exporter-trace-otlp-http",
|
|
223
|
+
"@opentelemetry/sdk-trace-base",
|
|
224
|
+
"@opentelemetry/resources",
|
|
225
|
+
"@opentelemetry/semantic-conventions",
|
|
226
|
+
"@opentelemetry/core",
|
|
227
|
+
"pino",
|
|
228
|
+
"pino-opentelemetry-transport",
|
|
229
|
+
];
|
|
230
|
+
console.info(` install: pnpm add ${packages.join(" ")}`);
|
|
231
|
+
if (isNextJs) {
|
|
232
|
+
console.info(" wire: Next.js instrumentation hook (instrumentation.ts in app root)");
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
console.info(" wire: node -r ./src/instrumentation.ts src/index.ts");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// 8. Install gate enforcement hooks
|
|
179
239
|
console.info("\n[Hooks]");
|
|
180
240
|
const hooksSource = join(packageRoot, "hooks");
|
|
181
241
|
const hooksTarget = join(projectRoot, ".claude/hooks");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "otel",
|
|
3
|
+
"description": "OpenTelemetry instrumentation — auto-instrumentation, Pino structured logging, category-based filtering",
|
|
4
|
+
"provides": {
|
|
5
|
+
"skill": true,
|
|
6
|
+
"health_checks": [
|
|
7
|
+
{
|
|
8
|
+
"name": "otel-instrumentation-exists",
|
|
9
|
+
"command": "test -f instrumentation.ts || test -f src/instrumentation.ts || test -f instrumentation.py"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "otel-packages-installed",
|
|
13
|
+
"command": "node -e \"require('@opentelemetry/sdk-node')\" 2>/dev/null || python -c \"import opentelemetry\" 2>/dev/null"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"verification": [
|
|
17
|
+
"test -f instrumentation.ts || test -f src/instrumentation.ts || test -f instrumentation.py"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"detect": {
|
|
21
|
+
"file": "instrumentation.ts"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# OpenTelemetry
|
|
2
|
+
|
|
3
|
+
Every service is instrumented from day one. `init` creates the instrumentation files. This skill teaches you how to use them.
|
|
4
|
+
|
|
5
|
+
## What's Already Set Up
|
|
6
|
+
|
|
7
|
+
After `init`, your project has:
|
|
8
|
+
- `instrumentation.ts` — auto-instruments HTTP, database, and framework calls
|
|
9
|
+
- `filtering-exporter.ts` — controls which span categories get exported
|
|
10
|
+
- `logger.ts` — Pino structured logger with stdout + OTLP dual output
|
|
11
|
+
|
|
12
|
+
## When to Add Manual Spans
|
|
13
|
+
|
|
14
|
+
Auto-instrumentation covers HTTP requests and database queries. You need manual spans for:
|
|
15
|
+
|
|
16
|
+
- **Business events**: `poker.hand.deal`, `auth.session.create`, `order.checkout.complete`
|
|
17
|
+
- **State transitions**: `game.state.waiting_to_active`, `payment.status.pending_to_settled`
|
|
18
|
+
- **Inference calls**: `inference.gemini.generate`, `inference.embedding.create`
|
|
19
|
+
- **Batch operations**: `migration.run`, `cron.cleanup.expired_sessions`
|
|
20
|
+
|
|
21
|
+
If it's a meaningful action that you'd want to see in a trace, add a span.
|
|
22
|
+
|
|
23
|
+
## Span Naming
|
|
24
|
+
|
|
25
|
+
Use `{domain}.{entity}.{action}`:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
const tracer = trace.getTracer('my-service');
|
|
29
|
+
|
|
30
|
+
// Good
|
|
31
|
+
tracer.startSpan('poker.hand.deal');
|
|
32
|
+
tracer.startSpan('auth.session.create');
|
|
33
|
+
tracer.startSpan('settlement.receipt.sign');
|
|
34
|
+
|
|
35
|
+
// Bad
|
|
36
|
+
tracer.startSpan('processRequest');
|
|
37
|
+
tracer.startSpan('handle');
|
|
38
|
+
tracer.startSpan('doThing');
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Custom Attributes
|
|
42
|
+
|
|
43
|
+
Always add domain-specific data to spans:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const span = tracer.startSpan('poker.hand.deal');
|
|
47
|
+
span.setAttribute('otel.category', 'business');
|
|
48
|
+
span.setAttribute('room.code', roomCode);
|
|
49
|
+
span.setAttribute('hand.number', handNumber);
|
|
50
|
+
span.setAttribute('player.count', players.length);
|
|
51
|
+
// ... do work ...
|
|
52
|
+
span.end();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Categories
|
|
56
|
+
|
|
57
|
+
Every manual span should get an `otel.category` attribute. This controls whether it gets exported:
|
|
58
|
+
|
|
59
|
+
| Category | What it covers | Examples |
|
|
60
|
+
|----------|---------------|----------|
|
|
61
|
+
| `http` | HTTP server/client requests | Auto-instrumented |
|
|
62
|
+
| `db` | Database queries | Auto-instrumented |
|
|
63
|
+
| `business` | Domain events | hand dealt, user registered, payment processed |
|
|
64
|
+
| `inference` | LLM/AI calls | Gemini generate, embedding create |
|
|
65
|
+
| `state` | State transitions | game started, order status changed |
|
|
66
|
+
| `system` | Infrastructure | health checks, cron jobs, queue processing |
|
|
67
|
+
|
|
68
|
+
Control which categories are exported via `OTEL_ENABLED_CATEGORIES`:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Export only HTTP and business spans
|
|
72
|
+
OTEL_ENABLED_CATEGORIES=http,business node -r ./instrumentation.ts src/index.ts
|
|
73
|
+
|
|
74
|
+
# Export everything (default)
|
|
75
|
+
node -r ./instrumentation.ts src/index.ts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Structured Logging with Pino
|
|
79
|
+
|
|
80
|
+
Use the project logger, not `console.log`:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { logger } from './logger';
|
|
84
|
+
|
|
85
|
+
// Good — structured, with context
|
|
86
|
+
logger.info({ roomCode, players: players.length }, 'hand started');
|
|
87
|
+
logger.error({ err, traceId: span.spanContext().traceId }, 'settlement failed');
|
|
88
|
+
logger.warn({ queueDepth: 150, threshold: 100 }, 'queue depth approaching limit');
|
|
89
|
+
|
|
90
|
+
// Bad — unstructured string
|
|
91
|
+
console.log('Hand started for room ' + roomCode);
|
|
92
|
+
console.log('Error: ' + err.message);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Log Levels
|
|
96
|
+
|
|
97
|
+
| Level | Meaning | When to use |
|
|
98
|
+
|-------|---------|------------|
|
|
99
|
+
| `error` | Something is broken | Unhandled errors, failed operations that should succeed |
|
|
100
|
+
| `warn` | Degraded but functional | Approaching limits, fallback behavior triggered |
|
|
101
|
+
| `info` | Business events | State transitions, user actions, completed operations |
|
|
102
|
+
| `debug` | Development details | Variable values, flow tracing — disable in production |
|
|
103
|
+
|
|
104
|
+
## Error Propagation
|
|
105
|
+
|
|
106
|
+
Errors must always include trace context:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
try {
|
|
110
|
+
await processSettlement(hand);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
span.recordException(err);
|
|
113
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
114
|
+
logger.error({ err, traceId: span.spanContext().traceId }, 'settlement failed');
|
|
115
|
+
throw err; // re-throw with context preserved
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Never swallow errors silently. Every catch block should either:
|
|
120
|
+
1. Re-throw with context
|
|
121
|
+
2. Log with trace correlation and handle completely
|
|
122
|
+
|
|
123
|
+
## Framework-Specific Notes
|
|
124
|
+
|
|
125
|
+
### Next.js
|
|
126
|
+
The `instrumentation.ts` file in the app root is loaded via the Next.js instrumentation hook (13.4+). No `-r` flag needed.
|
|
127
|
+
|
|
128
|
+
### Python
|
|
129
|
+
Use the `opentelemetry-instrument` CLI wrapper:
|
|
130
|
+
```bash
|
|
131
|
+
opentelemetry-instrument python your_app.py
|
|
132
|
+
```
|
|
133
|
+
Or import the instrumentation module before your app code.
|
|
134
|
+
|
|
135
|
+
## Environment Variables
|
|
136
|
+
|
|
137
|
+
| Variable | Purpose | Default |
|
|
138
|
+
|----------|---------|---------|
|
|
139
|
+
| `OTEL_SERVICE_NAME` | Service name in traces | Project name (set by init) |
|
|
140
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Backend URL | Not set (uses console exporter) |
|
|
141
|
+
| `OTEL_EXPORTER_OTLP_HEADERS` | Auth headers for backend | Not set |
|
|
142
|
+
| `OTEL_ENABLED_CATEGORIES` | Comma-separated active categories | All enabled |
|
|
143
|
+
| `LOG_LEVEL` | Pino log level | `info` |
|
|
144
|
+
|
|
145
|
+
When no `OTEL_EXPORTER_OTLP_ENDPOINT` is set, traces go to the console exporter (stdout). Enable a backend (like Dash0) to send traces remotely.
|
|
146
|
+
|
|
147
|
+
## Verification
|
|
148
|
+
|
|
149
|
+
During `/work`, check:
|
|
150
|
+
- Are new endpoints/business logic instrumented?
|
|
151
|
+
- Do manual spans have meaningful names and category attributes?
|
|
152
|
+
- Are errors recorded with trace context?
|
|
153
|
+
- Is structured logging used (not console.log)?
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FilteringExporter — Category-based span filtering for OpenTelemetry.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a real SpanExporter and drops spans from disabled categories
|
|
5
|
+
* before they reach the backend. This lets you instrument aggressively
|
|
6
|
+
* and control export volume at runtime.
|
|
7
|
+
*
|
|
8
|
+
* Categories are set via the `otel.category` span attribute.
|
|
9
|
+
* Spans without a category are always exported.
|
|
10
|
+
*
|
|
11
|
+
* Control which categories are active via OTEL_ENABLED_CATEGORIES env var:
|
|
12
|
+
* OTEL_ENABLED_CATEGORIES=http,business,inference
|
|
13
|
+
*
|
|
14
|
+
* If OTEL_ENABLED_CATEGORIES is not set, all categories are exported.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
ExportResult,
|
|
19
|
+
ExportResultCode,
|
|
20
|
+
} from "@opentelemetry/core";
|
|
21
|
+
import type { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
22
|
+
|
|
23
|
+
export const ALL_CATEGORIES = [
|
|
24
|
+
"http",
|
|
25
|
+
"db",
|
|
26
|
+
"business",
|
|
27
|
+
"inference",
|
|
28
|
+
"state",
|
|
29
|
+
"system",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export type OtelCategory = (typeof ALL_CATEGORIES)[number];
|
|
33
|
+
|
|
34
|
+
function getEnabledCategories(): Set<string> {
|
|
35
|
+
const env = process.env.OTEL_ENABLED_CATEGORIES;
|
|
36
|
+
if (!env) return new Set(ALL_CATEGORIES);
|
|
37
|
+
return new Set(
|
|
38
|
+
env
|
|
39
|
+
.split(",")
|
|
40
|
+
.map((c) => c.trim())
|
|
41
|
+
.filter(Boolean),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class FilteringExporter implements SpanExporter {
|
|
46
|
+
private enabledCategories: Set<string>;
|
|
47
|
+
|
|
48
|
+
constructor(private inner: SpanExporter) {
|
|
49
|
+
this.enabledCategories = getEnabledCategories();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export(
|
|
53
|
+
spans: ReadableSpan[],
|
|
54
|
+
resultCallback: (result: ExportResult) => void,
|
|
55
|
+
): void {
|
|
56
|
+
const filtered = spans.filter((span) => {
|
|
57
|
+
const category = span.attributes["otel.category"] as string | undefined;
|
|
58
|
+
// Spans without a category are always exported
|
|
59
|
+
if (!category) return true;
|
|
60
|
+
return this.enabledCategories.has(category);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (filtered.length > 0) {
|
|
64
|
+
this.inner.export(filtered, resultCallback);
|
|
65
|
+
} else {
|
|
66
|
+
resultCallback({ code: 0 as ExportResultCode });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async shutdown(): Promise<void> {
|
|
71
|
+
return this.inner.shutdown();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async forceFlush(): Promise<void> {
|
|
75
|
+
return this.inner.forceFlush?.() ?? Promise.resolve();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Re-read OTEL_ENABLED_CATEGORIES from env. Call after changing the env var at runtime. */
|
|
79
|
+
refreshCategories(): void {
|
|
80
|
+
this.enabledCategories = getEnabledCategories();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenTelemetry Auto-Instrumentation for Python
|
|
3
|
+
|
|
4
|
+
Run your application with the auto-instrumentation wrapper:
|
|
5
|
+
|
|
6
|
+
opentelemetry-instrument python your_app.py
|
|
7
|
+
|
|
8
|
+
Or import this module before your application code:
|
|
9
|
+
|
|
10
|
+
import instrumentation # noqa: F401
|
|
11
|
+
from your_app import main
|
|
12
|
+
main()
|
|
13
|
+
|
|
14
|
+
Configuration via environment variables:
|
|
15
|
+
OTEL_SERVICE_NAME — service name (defaults to "unknown-service")
|
|
16
|
+
OTEL_EXPORTER_OTLP_ENDPOINT — OTLP backend URL
|
|
17
|
+
OTEL_EXPORTER_OTLP_HEADERS — auth headers (e.g., "Authorization=Bearer xxx")
|
|
18
|
+
OTEL_PYTHON_LOG_CORRELATION — set to "true" for trace context in logs (default)
|
|
19
|
+
|
|
20
|
+
Install required packages:
|
|
21
|
+
pip install opentelemetry-distro opentelemetry-instrumentation opentelemetry-exporter-otlp
|
|
22
|
+
|
|
23
|
+
Then auto-install all available instrumentations:
|
|
24
|
+
opentelemetry-bootstrap --action=install
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import logging
|
|
29
|
+
|
|
30
|
+
from opentelemetry import trace
|
|
31
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
32
|
+
from opentelemetry.sdk.trace.export import (
|
|
33
|
+
BatchSpanProcessor,
|
|
34
|
+
ConsoleSpanExporter,
|
|
35
|
+
)
|
|
36
|
+
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
|
|
37
|
+
|
|
38
|
+
# Configure resource with service name
|
|
39
|
+
resource = Resource.create(
|
|
40
|
+
{SERVICE_NAME: os.environ.get("OTEL_SERVICE_NAME", "unknown-service")}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
provider = TracerProvider(resource=resource)
|
|
44
|
+
|
|
45
|
+
# Use OTLP exporter if endpoint is configured, otherwise console
|
|
46
|
+
otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
47
|
+
if otlp_endpoint:
|
|
48
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
49
|
+
OTLPSpanExporter,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
|
|
53
|
+
else:
|
|
54
|
+
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
|
55
|
+
|
|
56
|
+
trace.set_tracer_provider(provider)
|
|
57
|
+
|
|
58
|
+
# Enable trace context correlation in Python logging
|
|
59
|
+
os.environ.setdefault("OTEL_PYTHON_LOG_CORRELATION", "true")
|
|
60
|
+
|
|
61
|
+
# Configure structured logging
|
|
62
|
+
logging.basicConfig(
|
|
63
|
+
level=os.environ.get("LOG_LEVEL", "INFO").upper(),
|
|
64
|
+
format="%(asctime)s %(levelname)s [%(name)s] [trace_id=%(otelTraceID)s span_id=%(otelSpanID)s] %(message)s",
|
|
65
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry Auto-Instrumentation
|
|
3
|
+
*
|
|
4
|
+
* This file sets up automatic tracing for HTTP, database, and framework calls.
|
|
5
|
+
* Load it before your application code:
|
|
6
|
+
*
|
|
7
|
+
* node -r ./instrumentation.ts src/index.ts
|
|
8
|
+
*
|
|
9
|
+
* Or for Next.js, this file is automatically loaded via the instrumentation hook.
|
|
10
|
+
*
|
|
11
|
+
* Configuration via environment variables:
|
|
12
|
+
* OTEL_SERVICE_NAME — service name (defaults to "unknown-service")
|
|
13
|
+
* OTEL_EXPORTER_OTLP_ENDPOINT — OTLP backend URL (if not set, uses console exporter)
|
|
14
|
+
* OTEL_EXPORTER_OTLP_HEADERS — auth headers for the backend (e.g., "Authorization=Bearer xxx")
|
|
15
|
+
* OTEL_ENABLED_CATEGORIES — comma-separated categories to export (default: all)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
19
|
+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
20
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
21
|
+
import {
|
|
22
|
+
ConsoleSpanExporter,
|
|
23
|
+
BatchSpanProcessor,
|
|
24
|
+
SimpleSpanProcessor,
|
|
25
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
26
|
+
import { Resource } from "@opentelemetry/resources";
|
|
27
|
+
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
28
|
+
import { FilteringExporter } from "./filtering-exporter";
|
|
29
|
+
|
|
30
|
+
// Determine the span exporter based on whether an OTLP endpoint is configured
|
|
31
|
+
function createSpanProcessor() {
|
|
32
|
+
if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
|
|
33
|
+
// OTLP endpoint configured — send traces to backend, filtered by category
|
|
34
|
+
const otlpExporter = new OTLPTraceExporter();
|
|
35
|
+
const filtered = new FilteringExporter(otlpExporter);
|
|
36
|
+
return new BatchSpanProcessor(filtered);
|
|
37
|
+
}
|
|
38
|
+
// No backend — use console exporter for local development
|
|
39
|
+
return new SimpleSpanProcessor(new ConsoleSpanExporter());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sdk = new NodeSDK({
|
|
43
|
+
resource: new Resource({
|
|
44
|
+
[ATTR_SERVICE_NAME]:
|
|
45
|
+
process.env.OTEL_SERVICE_NAME ?? "unknown-service",
|
|
46
|
+
}),
|
|
47
|
+
spanProcessors: [createSpanProcessor()],
|
|
48
|
+
instrumentations: [
|
|
49
|
+
getNodeAutoInstrumentations({
|
|
50
|
+
// Disable fs instrumentation — too noisy, not useful for most apps
|
|
51
|
+
"@opentelemetry/instrumentation-fs": { enabled: false },
|
|
52
|
+
// Disable dns — rarely useful, adds noise
|
|
53
|
+
"@opentelemetry/instrumentation-dns": { enabled: false },
|
|
54
|
+
}),
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
sdk.start();
|
|
59
|
+
|
|
60
|
+
// Graceful shutdown on process exit
|
|
61
|
+
process.on("SIGTERM", () => {
|
|
62
|
+
sdk.shutdown().finally(() => process.exit(0));
|
|
63
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Logger — Pino with OpenTelemetry transport
|
|
3
|
+
*
|
|
4
|
+
* Dual output:
|
|
5
|
+
* 1. stdout — always, for local dev and Docker logs
|
|
6
|
+
* 2. OTLP — when OTEL_EXPORTER_OTLP_ENDPOINT is set, sends logs to backend
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { logger } from './logger';
|
|
10
|
+
* logger.info({ roomCode, players }, 'hand started');
|
|
11
|
+
* logger.error({ err, traceId }, 'settlement failed');
|
|
12
|
+
*
|
|
13
|
+
* Log levels:
|
|
14
|
+
* ERROR — something is broken, needs immediate attention
|
|
15
|
+
* WARN — degraded but functional, investigate soon
|
|
16
|
+
* INFO — business events, state transitions (the useful stuff)
|
|
17
|
+
* DEBUG — development details, disable in production
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import pino from "pino";
|
|
21
|
+
import type { TransportTargetOptions } from "pino";
|
|
22
|
+
|
|
23
|
+
const targets: TransportTargetOptions[] = [
|
|
24
|
+
// Always log to stdout
|
|
25
|
+
{
|
|
26
|
+
target: "pino/file",
|
|
27
|
+
options: { destination: 1 },
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// If an OTLP endpoint is configured, also send logs there
|
|
32
|
+
if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
|
|
33
|
+
targets.push({
|
|
34
|
+
target: "pino-opentelemetry-transport",
|
|
35
|
+
options: {
|
|
36
|
+
logRecordProcessorOptions: [
|
|
37
|
+
{
|
|
38
|
+
recordProcessorType: "batch",
|
|
39
|
+
exporterOptions: {
|
|
40
|
+
protocol: "http",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const logger = pino({
|
|
49
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
50
|
+
transport: { targets },
|
|
51
|
+
});
|