@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.
@@ -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. Install gate enforcement hooks
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infinitedusky/indusk-mcp",
3
- "version": "1.5.8",
3
+ "version": "1.5.9",
4
4
  "description": "InDusk development system — skills, MCP tools, and CLI for structured AI-assisted development",
5
5
  "type": "module",
6
6
  "files": [
@@ -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
+ });