Skip to content

Logging Webhooks — Complete Guide

DodaTech Updated 2026-06-28 8 min read

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

What log level should I use for webhook retries?

Use 'warn' for retries. The delivery is not yet failed but is experiencing issues. Track retry count in the log. Escalate to 'error' after exceeding max retries.

How long should webhook logs be retained?

30-90 days for debugging. 1-7 years for compliance (PCI-DSS, HIPAA, SOC2). Use hot storage (Elasticsearch) for 30 days, warm storage for 90 days, cold storage (S3/Glacier) for long-term retention.

Should I log webhook payloads in full?

Log payloads up to 10KB. Truncate larger payloads. Log the full payload only for failed deliveries at debug level. Never log sensitive data regardless of size.

How do I correlate webhook logs across services?

Use a correlation ID (UUID) generated at webhook receipt. Pass it through all downstream services: delivery, processing, storage. Include it in all log entries and API calls.

What is the difference between application logs and audit logs?

Application logs are for debugging and operations. They are rotated, truncated, and may be deleted. Audit logs are immutable, append-only records for compliance. They cannot be modified or deleted.

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