Skip to content

Logging

amqp-contract includes an optional, framework-agnostic logging abstraction. When you provide a Logger instance, both client and worker emit structured log messages for publish/consume operations, retries, errors, and more.

Logger Interface

The Logger type is exported from @amqp-contract/core:

typescript
import type { Logger, LoggerContext } from "@amqp-contract/core";
typescript
type LoggerContext = Record<string, unknown> & {
  error?: unknown;
};

type Logger = {
  debug(message: string, context?: LoggerContext): void;
  info(message: string, context?: LoggerContext): void;
  warn(message: string, context?: LoggerContext): void;
  error(message: string, context?: LoggerContext): void;
};

Each method receives a human-readable message and an optional context object containing structured data relevant to the log entry.

Usage with Client

Pass a logger to TypedAmqpClient.create():

typescript
import { TypedAmqpClient } from "@amqp-contract/client";

const client = (
  await TypedAmqpClient.create({
    contract,
    urls: ["amqp://localhost"],
    logger, 
  })
)._unsafeUnwrap();

Usage with Worker

Pass a logger to TypedAmqpWorker.create():

typescript
import { TypedAmqpWorker } from "@amqp-contract/worker";

const worker = (
  await TypedAmqpWorker.create({
    contract,
    urls: ["amqp://localhost"],
    handlers,
    logger, 
  })
)._unsafeUnwrap();

What Gets Logged

Client

LevelMessageContext
infoMessage published successfullypublisherName, exchange, routingKey, compressed

Worker

Consume Lifecycle

LevelMessageContext
infoMessage consumed successfullyconsumerName, queueName
warnConsumer cancelled by serverconsumerName, queueName
warnFailed to cancel consumer during closeconsumerTag, error
error{field} validation failedconsumerName, queueName, error
errorFailed to decompress messageconsumerName, queueName, error

Error Handling

LevelMessageContext
errorError processing messageconsumerName, queueName, errorType, error
errorNon-retryable error, sending to DLQ immediatelyconsumerName, errorType, error

Retry — Immediate-Requeue Mode

LevelMessageContext
infoMessage published for retryqueueName, retryCount
warnFailed to parse message for retry, using original bufferqueueName, error
warnRetrying message (immediate-requeue mode)consumerName, queueName, retryCount, maxRetries, error
errorMax retries exceeded, sending to DLQ (immediate-requeue mode)consumerName, queueName, retryCount, maxRetries, error
errorFailed to publish message for retry (write buffer full)queueName, retryCount

Retry — TTL-Backoff Mode

LevelMessageContext
infoMessage published for retryqueueName, retryCount, delayMs
warnFailed to parse message for retry, using original bufferqueueName, error
warnRetrying message (ttl-backoff mode)consumerName, queueName, retryCount, maxRetries, delayMs, error
errorMax retries exceeded, sending to DLQ (ttl-backoff mode)consumerName, queueName, retryCount, maxRetries, error
errorFailed to publish message for retry (write buffer full)queueName, retryCount, delayMs
errorQueue does not have TTL-backoff infrastructureconsumerName, queueName

Retry — None Mode (No retry)

LevelMessageContext
warnRetry disabled (none mode), sending to DLQconsumerName, error

Dead-Letter Queue

LevelMessageContext
warnQueue does not have DLX configured - message will be lost on nackqueueName
infoSending message to DLQqueueName, deliveryTag

Integration Examples

Pino

typescript
import pino from "pino";
import type { Logger } from "@amqp-contract/core";

const pinoLogger = pino({ name: "amqp" });

const logger: Logger = {
  debug: (message, context) => pinoLogger.debug(context, message),
  info: (message, context) => pinoLogger.info(context, message),
  warn: (message, context) => pinoLogger.warn(context, message),
  error: (message, context) => pinoLogger.error(context, message),
};

Winston

typescript
import winston from "winston";
import type { Logger } from "@amqp-contract/core";

const winstonLogger = winston.createLogger({
  level: "debug",
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});

const logger: Logger = {
  debug: (message, context) => winstonLogger.debug(message, context),
  info: (message, context) => winstonLogger.info(message, context),
  warn: (message, context) => winstonLogger.warn(message, context),
  error: (message, context) => winstonLogger.error(message, context),
};

Console

typescript
import type { Logger } from "@amqp-contract/core";

const logger: Logger = {
  debug: (message, context) => console.debug(message, context),
  info: (message, context) => console.info(message, context),
  warn: (message, context) => console.warn(message, context),
  error: (message, context) => console.error(message, context),
};

Logging vs OpenTelemetry

Logging and OpenTelemetry observability serve complementary purposes:

ConcernLoggingOpenTelemetry
PurposeHuman-readable operational messagesStructured traces, metrics, and context propagation
When to useDebugging, audit trails, operational monitoringDistributed tracing, performance dashboards, alerting
OverheadMinimal — synchronous string formattingSlightly higher — span creation, metric recording
DependenciesNone — bring your own loggerRequires @opentelemetry/api

Both can be enabled simultaneously — logging provides immediate human-readable output while OpenTelemetry provides deep observability across services.

Released under the MIT License.