Make functions observable with trace() wrapper, structured logging (Pino), and OpenTelemetry. Observability is orthogonal to business logic.
Adds OpenTelemetry tracing and Pino structured logging to functions using a trace() wrapper. Automatically instruments operations, maps Results to span status, and correlates logs with trace IDs for debugging production issues.
/plugin marketplace add jagreehal/jagreehal-claude-skills/plugin install jagreehal-claude-skills@jagreehal-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Observability implements the Observer pattern. Business logic is the subject (produces Results), tracing is the observer (watches without interfering).
PURE CORE (Subject)
├── Business logic: fn(args, deps): Result<T, E>
├── No knowledge of tracing
└── Returns Results, doesn't control observability
OBSERVER LAYER
├── trace() wrapper watches the flow
├── Result.ok -> span.setStatus(OK)
├── Result.error -> span.setStatus(ERROR)
└── Business logic remains "blind" to telemetry
NEVER use string interpolation. Use JSON fields:
// WRONG - Unstructured
deps.logger.info(`getUser called with userId=${args.userId}`);
// CORRECT - Structured
deps.logger.info({ userId: args.userId, action: 'getUser' }, 'getUser called');
Why Pino:
import pino from 'pino';
const logger = pino({
redact: ['password', 'apiKey', 'token', '*.secret', 'user.email'],
});
// Sensitive fields automatically stripped
logger.info({ event: 'login', user: req.body });
// Output: { "user": { "email": "[Redacted]", "password": "[Redacted]" } }
Use autotel for automatic spans:
import { trace, type TraceContext } from 'autotel';
const getUser = trace(
(ctx: TraceContext) => async (
args: { userId: string },
deps: GetUserDeps
) => {
ctx.setAttribute('user.id', args.userId);
const user = await deps.db.findUser(args.userId);
if (!user) {
ctx.setStatus({ code: 2, message: 'User not found' });
return err('NOT_FOUND');
}
ctx.setStatus({ code: 1 }); // OK
return ok(user);
}
);
Use OpenTelemetry standard attribute names:
// WRONG - Custom keys
ctx.setAttribute('userId', args.userId);
ctx.setAttribute('orderTotal', total);
// CORRECT - Semantic conventions
ctx.setAttribute('user.id', args.userId);
ctx.setAttribute('order.value', total);
| Standard Key | Instead Of | Why |
|---|---|---|
user.id | userId | Backends auto-correlate |
http.method | method | Automatic dashboards |
db.system | database | DB performance views |
error.type | errorCode | Error aggregation |
const getUser = trace((ctx) => async (args, deps) => {
const result = await getUserCore(args, deps);
if (!result.ok) {
ctx.setStatus({ code: 2, message: result.error });
} else {
ctx.setStatus({ code: 1 });
}
return result;
});
Pino redaction protects logs, but sensitive data can also leak into span attributes sent to Jaeger or Honeycomb:
// The problem: You set span attributes for debugging
ctx.setAttribute('user.email', user.email);
ctx.setAttribute('auth.token', req.headers.authorization); // 😱 Token in traces!
// The fix: Implement a global attribute filter
const SENSITIVE_KEYS = ['password', 'token', 'apiKey', 'secret', 'authorization'];
function sanitizeAttributes(attrs: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(
Object.entries(attrs).map(([key, value]) => {
const isSensitive = SENSITIVE_KEYS.some(k =>
key.toLowerCase().includes(k.toLowerCase())
);
return [key, isSensitive ? '[REDACTED]' : value];
})
);
}
init({
service: 'my-service',
attributeFilter: sanitizeAttributes,
});
SOC2 and GDPR compliance often require filtering at both layers. Defense in depth.
Include traceId and spanId in every log:
import { context, trace } from '@opentelemetry/api';
import pino from 'pino';
function createCorrelatedLogger() {
const baseLogger = pino();
return {
info: (obj: object, msg?: string) => {
const span = trace.getSpan(context.active());
const spanContext = span?.spanContext();
baseLogger.info({
...obj,
traceId: spanContext?.traceId,
spanId: spanContext?.spanId,
}, msg);
},
};
}
npm install autotel pino
import { init, trace, track } from 'autotel';
init({
service: 'my-service',
endpoint: process.env.OTEL_ENDPOINT,
debug: true, // Console output in development
});
| Backend | Endpoint |
|---|---|
| Jaeger | http://localhost:4318/v1/traces |
| Honeycomb | https://api.honeycomb.io (+ API key header) |
| Grafana Tempo | http://localhost:4318/v1/traces |
| Local dev | debug: true (console output) |
Use track() for business-level events (not spans):
import { track } from 'autotel';
// Track business events without creating spans
track('order.created', {
orderId: order.id,
customerId: order.customerId,
total: order.total,
itemCount: order.items.length,
});
track('user.signup', {
userId: user.id,
source: user.referralSource,
});
When to use trace() vs track():
trace() - Operations with duration (API calls, DB queries, workflows)track() - Point-in-time events (user actions, business milestones)Tests don't change. When tracing is disabled, trace() is a no-op:
it('returns user when found', async () => {
const mockUser = { id: '123', name: 'Alice' };
const deps = { db: { findUser: vi.fn().mockResolvedValue(mockUser) } };
const result = await getUser({ userId: '123' }, deps);
expect(result.ok).toBe(true);
});
// 1. Define deps type (unchanged)
type MyFunctionDeps = { db: Database; logger: Logger };
// 2. Wrap with trace(), keep deps explicit
const myFunction = trace(
(ctx: TraceContext) => async (args, deps: MyFunctionDeps) => {
ctx.setAttribute('key', value);
const result = await doWork(args, deps);
ctx.setStatus({ code: result.ok ? 1 : 2 });
return result;
}
);
// 3. Test without caring about tracing
const result = await myFunction(args, mockDeps);
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.