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".
Implement comprehensive observability for Documenso integrations including metrics, logging, and distributed tracing.
// 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,
};
// 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;
},
});
}
// 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;
// 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;
// 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;
}
// 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}`
);
}
// 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();
}
// 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 })
);
}
// 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;
# alerts/documenso.yml