Logging Webhooks — Complete Guide
In this tutorial, you will learn about Logging Webhooks. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn Webhook logging: structured logging for webhook delivery, log levels, request/response capture, centralized log aggregation, log-based alerting, and Compliance audit logging for webhooks.
What You Learn
You will learn how to implement structured logging for webhook systems, capture request and response details for debugging, centralize logs for analysis, use logs for alerting, and maintain compliance audit trails.
Why It Matters
Webhook delivery failures need investigation. Without detailed logs, you cannot determine if the provider sent the wrong payload, the consumer rejected it, or a network issue caused the failure. Structured logs with correlation IDs enable rapid debugging and compliance auditing.
Real-World Use
DodaTech's webhook system logs 500K delivery attempts daily with structured JSON logs. Each log includes correlation ID, subscriber ID, provider, event type, status code, duration, and error details. The centralized logging system retains logs for 90 days, supporting both debugging and compliance audits.
Structured Logging
const winston = require('winston');
// Structured logger for webhooks
const webhookLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: {
service: 'webhook-delivery',
environment: process.env.NODE_ENV || 'development',
},
transports: [
new winston.transports.Console(),
new winston.transports.File({
filename: 'logs/webhooks.json',
maxsize: 100 * 1024 * 1024, // 100MB
maxFiles: 10,
}),
],
});
// Log a webhook delivery attempt
function logWebhookDelivery({
webhookId,
subscriberId,
provider,
eventType,
url,
attempt,
status,
statusCode,
durationMs,
error,
requestHeaders,
responseHeaders,
responseBody,
payloadSize,
}) {
const logEntry = {
webhookId,
subscriberId,
provider,
eventType,
url,
attempt,
status,
statusCode,
durationMs,
payloadSize,
error,
};
// Include headers and response body only at debug level
if (status === 'failed' || status === 'error') {
logEntry.requestHeaders = sanitizeHeaders(requestHeaders);
logEntry.responseHeaders = sanitizeHeaders(responseHeaders);
logEntry.responseBody = truncateBody(responseBody, 1000);
}
const level = status === 'delivered' ? 'info'
: status === 'retrying' ? 'warn'
: 'error';
webhookLogger.log(level, 'Webhook delivery', logEntry);
}
function sanitizeHeaders(headers) {
if (!headers) return {};
const sensitive = ['authorization', 'cookie', 'set-cookie', 'x-api-key'];
const sanitized = { ...headers };
for (const key of sensitive) {
if (sanitized[key]) sanitized[key] = '[REDACTED]';
}
return sanitized;
}
function truncateBody(body, maxLength) {
if (!body) return null;
const str = typeof body === 'string' ? body : JSON.stringify(body);
return str.length > maxLength ? str.slice(0, maxLength) + '...' : str;
}
Expected output: Structured JSON logs capture every delivery attempt with context. Sensitive headers are redacted. Response bodies are truncated. Failed deliveries include full debug information.
Log Middleware for Express
const express = require('express');
const app = express();
const crypto = require('crypto');
// Request logging middleware
app.use('/webhooks', (req, res, next) => {
req.requestId = crypto.randomUUID();
req.startTime = Date.now();
// Capture original end to log response
const originalEnd = res.end;
res.end = function(...args) {
const duration = Date.now() - req.startTime;
webhookLogger.info('Webhook request', {
requestId: req.requestId,
method: req.method,
path: req.path,
provider: req.headers['user-agent'],
statusCode: res.statusCode,
durationMs: duration,
contentLength: req.headers['content-length'],
remoteAddr: req.ip,
});
originalEnd.apply(this, args);
};
next();
});
// Webhook endpoint with logging
app.post('/webhooks/stripe', async (req, res) => {
const webhookId = req.body?.id || 'unknown';
try {
// Process webhook
await processStripeWebhook(req.body);
webhookLogger.info('Webhook processed', {
requestId: req.requestId,
webhookId,
provider: 'stripe',
status: 'success',
});
res.status(200).send('OK');
} catch (err) {
webhookLogger.error('Webhook processing failed', {
requestId: req.requestId,
webhookId,
provider: 'stripe',
error: err.message,
stack: err.stack,
});
res.status(500).send('Error');
}
});
Expected output: Express middleware logs every webhook request with timing, status, and provider info. Failed processing logs full error details with stack trace.
Centralized Log Aggregation
// Log shipping to Elasticsearch
class ElasticsearchLogShipper {
constructor(options) {
this.node = options.node || 'http://localhost:9200';
this.index = options.index || 'webhook-logs';
}
async ship(logEntry) {
try {
await fetch(`${this.node}/${this.index}/_doc`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...logEntry,
'@timestamp': new Date().toISOString(),
}),
});
} catch (err) {
console.error('Failed to ship log to Elasticsearch:', err.message);
}
}
async bulkShip(logEntries) {
if (logEntries.length === 0) return;
const body = logEntries.flatMap(entry => [
JSON.stringify({ index: { _index: this.index } }),
JSON.stringify({
...entry,
'@timestamp': new Date().toISOString(),
}),
]).join('\n') + '\n';
try {
await fetch(`${this.node}/_bulk`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-ndjson' },
body,
});
} catch (err) {
console.error('Failed to bulk ship logs:', err.message);
}
}
}
Expected output: Logs are shipped to Elasticsearch for centralized search and analysis. Kibana dashboards provide log visualization. Full-text search enables debugging across millions of log entries.
Log-Based Alerting
// Log-based alert rules
const logAlertRules = [
{
name: 'RepeatedDeliveryFailures',
query: `
status:failed AND eventType:payment.*
| stats count by subscriberId
| where count > 5
`,
windowMinutes: 5,
severity: 'critical',
},
{
name: 'HighErrorRate',
query: `
level:error
| stats count by bin(@timestamp, 1m)
| where count > 100
`,
windowMinutes: 1,
severity: 'warning',
},
{
name: 'NovelErrorPattern',
query: `
level:error AND NOT error:"timeout"
| stats count by error
| where count == 1
`,
windowMinutes: 60,
severity: 'info',
},
];
class LogAlertEvaluator {
constructor(logStore) {
this.logStore = logStore;
this.alertedKeys = new Set();
}
async evaluate() {
for (const rule of logAlertRules) {
try {
const results = await this.logStore.search(rule.query, rule.windowMinutes);
if (results.length > 0) {
await this.fireAlert(rule, results);
}
} catch (err) {
console.error(`Alert evaluation failed: ${rule.name}`, err);
}
}
}
async fireAlert(rule, results) {
const alertKey = `${rule.name}:${JSON.stringify(results)}`;
if (this.alertedKeys.has(alertKey)) {
return; // Already alerted
}
this.alertedKeys.add(alertKey);
// Send alert to Slack/PagerDuty
console.log(`ALERT [${rule.severity}] ${rule.name}:`, results);
// Clear after 1 hour
setTimeout(() => this.alertedKeys.delete(alertKey), 3600000);
}
}
Expected output: Log-based alerting detects patterns that metrics may miss: repeated failures for a specific subscriber, novel error types, sudden error rate spikes. Alerts are deduplicated.
Compliance Audit Logging
// Immutable audit log for compliance
class WebhookAuditLogger {
constructor() {
this.auditLog = []; // In production: append-only database table
}
logWebhookReceived(webhook) {
this.append({
event: 'webhook.received',
webhookId: webhook.id,
provider: webhook.provider,
eventType: webhook.type,
payload: webhook.data,
timestamp: new Date().toISOString(),
sourceIp: webhook.sourceIp,
userAgent: webhook.userAgent,
});
}
logDeliveryAttempt(delivery) {
this.append({
event: 'webhook.delivery',
webhookId: delivery.webhookId,
subscriberId: delivery.subscriberId,
subscriberUrl: delivery.subscriberUrl,
attemptNumber: delivery.attempt,
status: delivery.status,
statusCode: delivery.statusCode,
durationMs: delivery.durationMs,
timestamp: new Date().toISOString(),
});
}
logManualAction(action) {
this.append({
event: 'webhook.manual_action',
action: action.type, // retry, delete, modify
webhookId: action.webhookId,
performedBy: action.user,
reason: action.reason,
timestamp: new Date().toISOString(),
});
}
append(entry) {
// In production: INSERT INTO audit_log (data) VALUES ($1)
this.auditLog.push(entry);
console.log('AUDIT:', JSON.stringify(entry));
}
query(options = {}) {
const { webhookId, subscriberId, event, startDate, endDate, limit } = options;
let results = this.auditLog;
if (webhookId) results = results.filter(e => e.webhookId === webhookId);
if (subscriberId) results = results.filter(e => e.subscriberId === subscriberId);
if (event) results = results.filter(e => e.event === event);
if (startDate) results = results.filter(e => e.timestamp >= startDate);
if (endDate) results = results.filter(e => e.timestamp <= endDate);
return results.slice(-(limit || 100));
}
}
Expected output: Audit log captures every webhook event immutably. Each entry has a timestamp, actor, and action. Compliance queries can trace exactly what happened to any webhook.
Common Mistakes
1. Logging Sensitive Data
Logging full payloads with API keys, passwords, or PII is a compliance violation. Sanitize logs: redact sensitive headers, truncate payloads, strip credit card numbers.
2. No Correlation ID
Without a correlation ID across the entire webhook lifecycle, you cannot trace a webhook from receipt to delivery to processing. Use a unique ID that flows through all logs.
3. Logging Too Much or Too Little
Logging every detail at info level creates noise. Log success at info, retries at warn, failures at error. Include full debug context (headers, body, stack trace) only on failures.
4. No Log Rotation
Unrotated logs fill disks and crash the application. Implement log rotation by size and time. Archive old logs to cold storage. Use centralized logging to avoid local log management.
5. Not Using Structured Logs
Plain text logs cannot be queried or parsed by log aggregation systems. Use structured JSON logs. Include timestamps, levels, and metadata for filtering and searching.
Practice Questions
1. What is structured logging and why is it important for webhooks?
Structured logging outputs JSON objects instead of plain text. Log aggregation systems can query, filter, and visualize structured logs. Webhook debugging requires filtering by subscriber, event type, and status.
2. What should be included in a webhook delivery log?
Correlation ID, webhook ID, subscriber ID, provider, event type, URL, attempt number, status, status code, duration, payload size, error message. Include request/response details only on failure.
3. How do you handle sensitive data in webhook logs?
Redact sensitive headers (authorization, cookie). Truncate response bodies to 1000 characters. Never log credit card numbers, passwords, or API keys. Use data masking for PII.
4. Why use a centralized logging system for webhooks?
Centralized logging aggregates logs from multiple instances. You can search across all instances, correlate events, build dashboards, and set up log-based alerts. Without centralization, debugging is per-instance.
Challenge
Build a complete webhook logging system: structured JSON logging with Winston, correlation IDs across all services, Express middleware for automatic request/response logging, centralized log shipping to Elasticsearch, Kibana dashboard for log search and visualization, and log-based alerting for failure patterns.
FAQ
Mini Project: Centralized Webhook Logging
Build a centralized logging system with: structured JSON logging from all webhook services, correlation ID propagation, log shipping to Elasticsearch, Kibana dashboards for webhook delivery analysis, log-based alerting for failure patterns, and immutable audit logs for compliance with 1-year retention.
What's Next
Now that you understand logging, learn about Webhook Security for IP whitelisting and additional security measures.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro