| name | documenso-observability |
| description | Implement monitoring, logging, and tracing for Documenso integrations.
Use when setting up observability, implementing metrics collection,
or debugging production issues.
Trigger with phrases like "documenso monitoring", "documenso metrics",
"documenso logging", "documenso tracing", "documenso observability".
|
| allowed-tools | Read, Write, Edit |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
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
import { Counter, Histogram, Gauge, Registry } from "prom-client";
const registry = new Registry();
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],
});
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],
});
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
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) {
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)}`;
timer = requestDuration.({ operation });
{
result = method.(nestedTarget, args);
requestCounter.({ operation, : });
result;
} (: ) {
status =
error. === ? : ;
requestCounter.({ operation, status });
(error. === ) {
rateLimitHits.();
}
errorCounter.({
operation,
: (error. ?? ),
});
error;
} {
();
}
};
}
method;
},
});
}
value;
},
});
}
Step 3: Metrics Endpoint
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
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,
},
});
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
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 };
delete sanitized.;
sanitized.;
(sanitized.) {
sanitized. = sanitized..( ({
: r.,
: r.,
}));
}
sanitized;
}
Step 6: Webhook Logging
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
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.();
.();
}
(): <> {
sdk.();
}
Step 8: Custom Spans for Documenso Operations
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.(error);
error;
} {
span.();
}
}
);
}
() {
(
,
{ : title },
client..({ title })
);
}
Health Check Endpoint
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,
},
},
};
const start = Date.now();
try {
const client = getDocumensoClient();
await client..({ : });
health.. = {
: ,
: .() - start,
};
} (: ) {
health.. = {
: ,
: .() - start,
: error.,
};
health. = ;
}
statusCode = health. === ? : ;
res.(statusCode).(health);
});
router;
Alerting Rules
Prometheus Alerting Rules
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
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":
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.