Claude-skill-registry documenso-observability
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/documenso-observability" ~/.claude/skills/majiayu000-claude-skill-registry-documenso-observability && rm -rf "$T"
manifest:
skills/data/documenso-observability/SKILL.mdsource content
Documenso Observability
Overview
Implement comprehensive observability for Documenso integrations including metrics, logging, and distributed tracing.
Prerequisites
- Working Documenso integration
- Monitoring platform (Datadog, Prometheus, etc.)
- Logging infrastructure (ELK, CloudWatch, etc.)
- Tracing system (Jaeger, Zipkin, etc.)
Metrics Collection
Step 1: Core Metrics Definition
// src/observability/metrics.ts import { Counter, Histogram, Gauge, Registry } from "prom-client"; // Create custom registry const registry = new Registry(); // Request metrics const requestCounter = new Counter({ name: "documenso_requests_total", help: "Total number of Documenso API requests", labelNames: ["operation", "status"], registers: [registry], }); const requestDuration = new Histogram({ name: "documenso_request_duration_seconds", help: "Documenso API request duration in seconds", labelNames: ["operation"], buckets: [0.1, 0.25, 0.5, 1, 2.5, 5, 10], registers: [registry], }); // Document metrics const documentsCreated = new Counter({ name: "documenso_documents_created_total", help: "Total documents created", registers: [registry], }); const documentsCompleted = new Counter({ name: "documenso_documents_completed_total", help: "Total documents completed", registers: [registry], }); const activeDocuments = new Gauge({ name: "documenso_active_documents", help: "Number of documents pending signatures", registers: [registry], }); // Error metrics const errorCounter = new Counter({ name: "documenso_errors_total", help: "Total Documenso errors", labelNames: ["operation", "error_code"], registers: [registry], }); const rateLimitHits = new Counter({ name: "documenso_rate_limit_hits_total", help: "Total rate limit (429) responses", registers: [registry], }); export { registry, requestCounter, requestDuration, documentsCreated, documentsCompleted, activeDocuments, errorCounter, rateLimitHits, };
Step 2: Instrumented Client Wrapper
// src/observability/instrumented-client.ts import { Documenso } from "@documenso/sdk-typescript"; import { requestCounter, requestDuration, errorCounter, rateLimitHits, } from "./metrics"; export function createInstrumentedClient( baseClient: Documenso ): Documenso { return new Proxy(baseClient, { get(target, prop) { const value = (target as any)[prop]; if (typeof value === "object" && value !== null) { // Proxy nested objects (documents, templates, etc.) return new Proxy(value, { get(nestedTarget, nestedProp) { const method = (nestedTarget as any)[nestedProp]; if (typeof method === "function") { return async (...args: any[]) => { const operation = `${String(prop)}.${String(nestedProp)}`; const timer = requestDuration.startTimer({ operation }); try { const result = await method.apply(nestedTarget, args); requestCounter.inc({ operation, status: "success" }); return result; } catch (error: any) { const status = error.statusCode === 429 ? "rate_limited" : "error"; requestCounter.inc({ operation, status }); if (error.statusCode === 429) { rateLimitHits.inc(); } errorCounter.inc({ operation, error_code: String(error.statusCode ?? "unknown"), }); throw error; } finally { timer(); } }; } return method; }, }); } return value; }, }); }
Step 3: Metrics Endpoint
// src/api/metrics.ts import express from "express"; import { registry } from "../observability/metrics"; const router = express.Router(); router.get("/metrics", async (req, res) => { try { res.set("Content-Type", registry.contentType); res.end(await registry.metrics()); } catch (error) { res.status(500).end(); } }); export default router;
Structured Logging
Step 4: Logger Configuration
// src/observability/logger.ts import pino from "pino"; const logger = pino({ level: process.env.LOG_LEVEL ?? "info", formatters: { level: (label) => ({ level: label }), }, base: { service: "signing-service", environment: process.env.NODE_ENV, }, redact: { paths: [ "apiKey", "signingToken", "signingUrl", "*.apiKey", "*.signingToken", "req.headers.authorization", ], remove: true, }, }); // Create child loggers for different modules export const documensoLogger = logger.child({ module: "documenso" }); export const webhookLogger = logger.child({ module: "webhook" }); export const jobLogger = logger.child({ module: "jobs" }); export default logger;
Step 5: Request Logging
// src/observability/request-logger.ts import { documensoLogger } from "./logger"; export function logDocumensoRequest( operation: string, params: Record<string, any>, result: "success" | "error", duration: number, error?: Error ): void { const logData = { operation, params: sanitizeParams(params), result, durationMs: duration, }; if (result === "success") { documensoLogger.info(logData, `Documenso ${operation} succeeded`); } else { documensoLogger.error( { ...logData, error: error?.message }, `Documenso ${operation} failed` ); } } function sanitizeParams(params: Record<string, any>): Record<string, any> { const sanitized = { ...params }; // Remove sensitive fields delete sanitized.apiKey; delete sanitized.file; // Don't log file contents // Truncate large fields if (sanitized.recipients) { sanitized.recipients = sanitized.recipients.map((r: any) => ({ email: r.email, role: r.role, })); } return sanitized; }
Step 6: Webhook Logging
// src/webhooks/logging.ts import { webhookLogger } from "../observability/logger"; export function logWebhookReceived( event: string, documentId: string, validSignature: boolean ): void { webhookLogger.info( { event, documentId, validSignature, timestamp: new Date().toISOString(), }, `Webhook received: ${event}` ); } export function logWebhookProcessed( event: string, documentId: string, duration: number, success: boolean ): void { const level = success ? "info" : "error"; webhookLogger[level]( { event, documentId, durationMs: duration, success, }, `Webhook ${success ? "processed" : "failed"}: ${event}` ); }
Distributed Tracing
Step 7: OpenTelemetry Setup
// src/observability/tracing.ts import { NodeSDK } from "@opentelemetry/sdk-node"; import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; import { Resource } from "@opentelemetry/resources"; import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; const sdk = new NodeSDK({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: "signing-service", [SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION ?? "unknown", }), traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://localhost:4318/v1/traces", }), instrumentations: [getNodeAutoInstrumentations()], }); export function initTracing(): void { sdk.start(); console.log("OpenTelemetry tracing initialized"); } export function shutdownTracing(): Promise<void> { return sdk.shutdown(); }
Step 8: Custom Spans for Documenso Operations
// src/observability/spans.ts import { trace, SpanStatusCode, Span } from "@opentelemetry/api"; const tracer = trace.getTracer("documenso-client"); export async function withDocumensoSpan<T>( operation: string, attributes: Record<string, string>, fn: () => Promise<T> ): Promise<T> { return tracer.startActiveSpan( `documenso.${operation}`, { attributes: { "documenso.operation": operation, ...attributes, }, }, async (span: Span) => { try { const result = await fn(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error: any) { span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); span.recordException(error); throw error; } finally { span.end(); } } ); } // Usage example async function createDocumentWithTracing(title: string) { return withDocumensoSpan( "documents.create", { "document.title": title }, () => client.documents.createV0({ title }) ); }
Health Check Endpoint
// src/api/health.ts import express from "express"; import { getDocumensoClient } from "../documenso/client"; const router = express.Router(); interface HealthStatus { status: "healthy" | "degraded" | "unhealthy"; timestamp: string; checks: { documenso: { status: string; latencyMs: number; error?: string; }; }; } router.get("/health", async (req, res) => { const health: HealthStatus = { status: "healthy", timestamp: new Date().toISOString(), checks: { documenso: { status: "unknown", latencyMs: 0, }, }, }; // Check Documenso connectivity const start = Date.now(); try { const client = getDocumensoClient(); await client.documents.findV0({ perPage: 1 }); health.checks.documenso = { status: "healthy", latencyMs: Date.now() - start, }; } catch (error: any) { health.checks.documenso = { status: "unhealthy", latencyMs: Date.now() - start, error: error.message, }; health.status = "unhealthy"; } const statusCode = health.status === "unhealthy" ? 503 : 200; res.status(statusCode).json(health); }); export default router;
Alerting Rules
Prometheus Alerting Rules
# alerts/documenso.yml groups: - name: documenso rules: - alert: DocumensoHighErrorRate expr: | sum(rate(documenso_errors_total[5m])) / sum(rate(documenso_requests_total[5m])) > 0.05 for: 5m labels: severity: critical annotations: summary: "High Documenso error rate" description: "Error rate is {{ $value | humanizePercentage }}" - alert: DocumensoHighLatency expr: | histogram_quantile(0.95, sum(rate(documenso_request_duration_seconds_bucket[5m])) by (le) ) > 5 for: 5m labels: severity: warning annotations: summary: "High Documenso API latency" description: "P95 latency is {{ $value }}s" - alert: DocumensoRateLimited expr: | sum(rate(documenso_rate_limit_hits_total[5m])) > 0 for: 1m labels: severity: warning annotations: summary: "Documenso rate limiting detected" - alert: DocumensoServiceDown expr: up{job="signing-service"} == 0 for: 1m labels: severity: critical annotations: summary: "Signing service is down"
Grafana Dashboard
{ "title": "Documenso Integration", "panels": [ { "title": "Request Rate", "type": "graph", "targets": [ { "expr": "sum(rate(documenso_requests_total[5m])) by (operation)" } ] }, { "title": "Error Rate", "type": "graph", "targets": [ { "expr": "sum(rate(documenso_errors_total[5m])) by (error_code)" } ] }, { "title": "P95 Latency", "type": "graph", "targets": [ { "expr": "histogram_quantile(0.95, sum(rate(documenso_request_duration_seconds_bucket[5m])) by (le, operation))" } ] }, { "title": "Documents Created", "type": "stat", "targets": [ { "expr": "sum(increase(documenso_documents_created_total[24h]))" } ] } ] }
Output
- Prometheus metrics exposed
- Structured logging configured
- Distributed tracing enabled
- Health checks implemented
- Alerting rules defined
Error Handling
| Observability Issue | Cause | Solution |
|---|---|---|
| Metrics not showing | Wrong scrape config | Check Prometheus config |
| Logs not appearing | Log level too high | Set LOG_LEVEL=debug |
| Traces missing | OTEL not initialized | Call initTracing() |
| High cardinality | Too many labels | Reduce label values |
Resources
Next Steps
For incident response, see
documenso-incident-runbook.