open-wa
DocsLogging and audit trails

Logging and audit trails

Keep useful open-wa diagnostics, rotate application logs, and define an honest audit record.

Logging and audit trails

open-wa plugin logs go through the host logger. Your application can also create its own Winston logger for selected events, but that second logger does not automatically capture the host's stdout or plugin logs. Decide which stream owns each record before you choose a retention or audit process.

Host and plugin logs

Plugins receive a scoped logger:

init: async ({ logger }) => {
  logger.info('Plugin initialized', { version: '1.0.0' });
  logger.warn('API key is missing');
  logger.error('Failed to connect', { code: 'ECONNREFUSED' });
  logger.debug('Processing message', { messageId: 'redacted-id' });
}

By default those records go to the Easy API process's stdout/stderr. Capture that stream with your process supervisor or shell:

The explicit flags below start a local logs session and bind the API to loopback. Set the session ID and host to the values your application uses.

mkdir -p ./logs
npx @open-wa/wa-automate@5.1.0 --session-id logs --host 127.0.0.1 --port 8080 \
  >> ./logs/open-wa.log 2>&1

The host's output format and persistence are deployment choices. Redirecting stdout does not capture a separately configured application logger, and creating a Winston logger does not change how host logs are stored.

Application-owned rotating logs

Install both the logger and the transport used by the sample:

npm install winston winston-daily-rotate-file
export OPENWA_SESSION_ID='sales'
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';

const rotate = new DailyRotateFile({
  filename: './logs/open-wa-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  maxFiles: '14d',
  maxSize: '50m',
  zippedArchive: true,
});

const sessionId = process.env.OPENWA_SESSION_ID;
if (!sessionId) throw new Error('Set OPENWA_SESSION_ID to the Easy API session ID');

rotate.on('error', (error) => {
  // Surface a transport failure to the process supervisor; do not silently lose it.
  console.error('rotating log transport failed', error);
});

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [rotate],
});

logger.on('error', (error) => {
  console.error('application logger failed', error);
});

export function recordMessage(message) {
  logger.info('message.received', {
    messageId: message.id,
    sessionId,
    chatId: message.chatId,
    timestamp: new Date().toISOString(),
  });
}

Pass recordMessage to your Client instance's onMessage listener when you want this record for each incoming message.

The winston-daily-rotate-file import is required because DailyRotateFile is not provided by winston alone. The example records identifiers and routing fields; add message content only when the business purpose and access policy require it.

Structured output and queries

When your logger writes JSON, filter the file with jq:

jq 'select(.message == "message.received")' ./logs/open-wa-*.log

If the command reports no matches, inspect the file path and logger level before changing application code:

ls -lh ./logs
tail -n 50 ./logs/open-wa-*.log

Store a narrow audit record

For a small app-owned journal, SQLite can store identifiers and timestamps without copying message bodies. Install better-sqlite3, create the table once, then insert only the fields your workflow needs:

import Database from 'better-sqlite3';

const db = new Database('./logs/audit.sqlite');
db.exec(`
  CREATE TABLE IF NOT EXISTS message_audit (
    session_id TEXT NOT NULL,
    message_id TEXT NOT NULL,
    chat_id TEXT NOT NULL,
    observed_at INTEGER NOT NULL,
    PRIMARY KEY (session_id, message_id)
  )
`);
const insertAudit = db.prepare(`
  INSERT OR IGNORE INTO message_audit (session_id, message_id, chat_id, observed_at)
  VALUES (?, ?, ?, ?)
`);

function recordMessage(message) {
  insertAudit.run(sessionId, message.id, message.chatId, Date.now());
}

Install the database package alongside the logger dependencies with npm install better-sqlite3. This is an application record, not proof of regulatory compliance. Define who can read or delete it, how long it is retained, and whether your policy requires tamper evidence, legal hold, or a specific storage region. Keep tokens, QR values, session credentials, and complete message bodies out of logs unless a documented purpose requires them.

Troubleshooting

SymptomCommand or observationExpected resultNext action
No host logs appearmkdir -p ./logs && npx @open-wa/wa-automate@5.1.0 --session-id logs --host 127.0.0.1 --port 8080 >> ./logs/open-wa.log 2>&1Startup lines appear in ./logs/open-wa.log.Check the working directory, permissions, and process-supervisor stdout capture; run the process in the foreground when you need to see stderr directly.
Host log exists but rotating file is emptyls -lh ./logs; tail -n 50 ./logs/open-wa-*.logThe application logger's file exists and contains JSON records after recordMessage runs.Confirm winston-daily-rotate-file is installed, the import is present, and the app logger is actually called; host logs use a separate stream.
Transport emits an errorInspect stderr for rotating log transport failedThe error includes the path or filesystem cause.Fix directory ownership, free disk space, or change the path, then restart the process.
A webhook is retriedInspect the webhook sender log for Webhook delivery failedThe line includes an attempt count and delay.Make the receiver return a fast 2xx; use its durable journal if restart replay is required.
Audit rows exist but cannot support a policy claimReview the stored columns and retention jobRows contain identifiers and timestamps, but no automatic tamper or deletion controls.Add the deployment's access, retention, deletion, regional, and evidence controls before calling the process compliant.

Retention is an explicit policy

A retention job should be scoped to the fields and policy your application owns:

const cutoff = Date.now() - 90 * 24 * 60 * 60 * 1000;
const result = db.prepare('DELETE FROM message_audit WHERE observed_at < ?').run(cutoff);
console.info('expired audit rows removed', result.changes);

This continues the SQLite example above. Ninety days is an example, not a legal requirement; choose a period that matches your data inventory, customer commitments, deletion requests, incident response, and provider lifecycle controls.

Was this helpful?

Your answer includes the page path and docs version.

On this page