@contract-kit/provider-logger-pino
v0.1.1
Published
Pino logger provider for contract-kit - adds logger port using pino
Maintainers
Readme
@contract-kit/provider-logger-pino
Pino logger provider for contract-kit that extends your application ports with structured logging capabilities using Pino.
Installation
npm install @contract-kit/provider-logger-pino pino
# or
bun add @contract-kit/provider-logger-pino pinoFor pretty-printed logs in development (optional):
npm install --save-dev pino-pretty
# or
bun add -d pino-prettyTypeScript Requirements
This package requires TypeScript 5.0 or higher for proper type inference.
Usage
Basic Setup
import { createServer } from "@contract-kit/server";
import { loggerPinoProvider } from "@contract-kit/provider-logger-pino";
// Set environment variables:
// LOG_LEVEL=info
// LOG_FORMAT=json
// LOG_SERVICE=my-app (optional)
// LOG_TIMESTAMP=true
const app = createServer({
ports: basePorts,
providers: [loggerPinoProvider],
createContext: ({ ports }) => ({
ports,
// ... other context
}),
routes: [
// ... your routes
],
});Using the Logger in Use Cases
Once the provider is registered, your ports will include a logger property:
// Log informational messages
async function createUser(ctx: AppCtx, email: string) {
ctx.ports.logger.info("Creating user", { email });
const user = await ctx.ports.db.users.create({ email });
ctx.ports.logger.info("User created successfully", {
userId: user.id,
email: user.email
});
return user;
}
// Log errors with context
async function processPayment(ctx: AppCtx, amount: number) {
try {
await ctx.ports.payment.charge(amount);
ctx.ports.logger.info("Payment processed", { amount });
} catch (error) {
ctx.ports.logger.error("Payment failed", {
amount,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
// Log warnings
async function checkQuota(ctx: AppCtx, userId: string) {
const usage = await ctx.ports.db.usage.getByUser(userId);
if (usage.remaining < 10) {
ctx.ports.logger.warn("User approaching quota limit", {
userId,
remaining: usage.remaining,
total: usage.total
});
}
}Request-Scoped Child Loggers
Create child loggers with bound fields for request-scoped logging:
// In your request handler or middleware
async function handleRequest(ctx: AppCtx, requestId: string) {
// Create child logger with request context
const log = ctx.ports.logger.child({
requestId,
userId: ctx.userId
});
log.info("Processing request");
try {
const result = await processRequest(ctx);
log.info("Request completed", { duration: Date.now() - start });
return result;
} catch (error) {
log.error("Request failed", {
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
// All logs from the child logger will include requestId and userIdAll Log Levels
The logger supports six log levels, from most to least verbose:
ctx.ports.logger.trace("Very detailed diagnostic info", { data });
ctx.ports.logger.debug("Debugging information", { state });
ctx.ports.logger.info("General informational messages", { event });
ctx.ports.logger.warn("Warning conditions", { issue });
ctx.ports.logger.error("Error conditions", { error });
ctx.ports.logger.fatal("Fatal errors that will terminate the app", { reason });Configuration
The logger provider reads configuration from environment variables with the LOG_ prefix:
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| LOG_LEVEL | No | info | Minimum log level: trace, debug, info, warn, error, fatal |
| LOG_FORMAT | No | json | Log format: json (structured) or pretty (human-readable) |
| LOG_SERVICE | No | - | Service name to add to all log entries |
| LOG_TIMESTAMP | No | true | Whether to include timestamps (true or false) |
Log Levels
Set LOG_LEVEL to control which messages are logged:
trace: Most verbose, logs everything including trace messagesdebug: Logs debug and above (excludes trace)info: Logs info and above (default, good for production)warn: Logs warnings, errors, and fatal onlyerror: Logs errors and fatal onlyfatal: Logs only fatal messages
Log Formats
JSON Format (Production)
LOG_FORMAT=jsonOutputs newline-delimited JSON, perfect for log aggregation systems:
{"level":30,"time":"2024-01-15T10:30:00.000Z","service":"my-app","msg":"User logged in","userId":123}
{"level":40,"time":"2024-01-15T10:30:05.000Z","service":"my-app","msg":"Rate limit approaching","remaining":10}Pretty Format (Development)
LOG_FORMAT=prettyRequires pino-pretty to be installed. Outputs human-readable logs:
[10:30:00.000] INFO: User logged in
userId: 123
[10:30:05.000] WARN: Rate limit approaching
remaining: 10If pino-pretty is not installed, the logger will automatically fall back to JSON format and log a warning.
Service Name
Add a service name to all log entries for easier filtering in multi-service environments:
LOG_SERVICE=api-gatewayAll logs will include "service":"api-gateway":
{"level":30,"time":"2024-01-15T10:30:00.000Z","service":"api-gateway","msg":"Request received"}Timestamps
Control whether timestamps are included in logs:
LOG_TIMESTAMP=false # Disable timestamps
LOG_TIMESTAMP=true # Enable timestamps (default)Logger Port API
The provider extends your ports with the following logger interface:
trace(message: string, meta?: Record<string, unknown>): void
Log trace-level messages (most verbose). Use for very detailed diagnostic information.
ctx.ports.logger.trace("Function entered", { args: [1, 2, 3] });debug(message: string, meta?: Record<string, unknown>): void
Log debug-level messages. Use for diagnostic information useful during development.
ctx.ports.logger.debug("Cache miss", { key: "user:123" });info(message: string, meta?: Record<string, unknown>): void
Log info-level messages. Use for general informational messages about application flow.
ctx.ports.logger.info("User logged in", { userId: 123 });warn(message: string, meta?: Record<string, unknown>): void
Log warning-level messages. Use for warning conditions that should be reviewed.
ctx.ports.logger.warn("API rate limit approaching", { remaining: 10 });error(message: string, meta?: Record<string, unknown>): void
Log error-level messages. Use for error conditions that need attention.
ctx.ports.logger.error("Database connection failed", {
error: error.message,
retryAttempt: 3
});fatal(message: string, meta?: Record<string, unknown>): void
Log fatal-level messages (most severe). Use for errors that will cause the application to terminate.
ctx.ports.logger.fatal("Out of memory", {
heapUsed: process.memoryUsage().heapUsed
});child(bindings: Record<string, unknown>): LoggerPort
Create a child logger with bound fields. All messages logged via the child will include these fields.
const requestLogger = ctx.ports.logger.child({
requestId: "abc123",
userId: ctx.userId
});
requestLogger.info("Processing request");
// Logs: { requestId: "abc123", userId: 456, msg: "Processing request", ... }TypeScript Support
To get proper type inference for the logger port, extend your ports type:
import type { LoggerPort } from "@contract-kit/provider-logger-pino";
// Your base ports
const basePorts = definePorts({
db: dbAdapter,
});
// After using loggerPinoProvider, your ports will have this shape:
type AppPorts = typeof basePorts & {
logger: LoggerPort;
};Example: Complete Setup
import { createServer } from "@contract-kit/server";
import { loggerPinoProvider } from "@contract-kit/provider-logger-pino";
import { definePorts } from "@contract-kit/ports";
// Define base ports
const basePorts = definePorts({
db: myDatabaseAdapter,
});
// Create app with logger provider
const app = createServer({
ports: basePorts,
providers: [loggerPinoProvider],
createContext: ({ ports, req }) => ({
ports,
requestId: req.headers["x-request-id"] || crypto.randomUUID(),
}),
routes: [
// ... your routes
],
});
// Use in your application
async function myUseCase(ctx: AppCtx) {
// Create request-scoped logger
const log = ctx.ports.logger.child({ requestId: ctx.requestId });
log.info("Starting operation");
try {
const result = await ctx.ports.db.query("...");
log.info("Operation completed", { resultCount: result.length });
return result;
} catch (error) {
log.error("Operation failed", {
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}Lifecycle
The Pino logger provider:
- During
register:- Validates configuration from environment variables
- Creates Pino logger instance with specified options
- Adds the
loggerport to your application
The provider does not implement onAppStop as Pino automatically flushes logs and does not require explicit cleanup.
Best Practices
1. Use Structured Logging
Always pass structured metadata instead of interpolating values into messages:
// ✅ Good: Structured metadata
ctx.ports.logger.info("User login attempt", {
email: user.email,
success: true,
ip: req.ip
});
// ❌ Bad: String interpolation
ctx.ports.logger.info(`User ${user.email} logged in from ${req.ip}`);2. Use Appropriate Log Levels
trace/debug: Development and troubleshooting onlyinfo: Normal application flow (e.g., user actions, important state changes)warn: Unusual but handled conditions (e.g., rate limits, retries)error: Errors that need attention but are handledfatal: Unrecoverable errors (e.g., out of memory, critical dependency failure)
3. Use Child Loggers for Context
Create child loggers with request/operation context:
const log = ctx.ports.logger.child({
requestId: ctx.requestId,
userId: ctx.userId,
operation: "checkout"
});
// All subsequent logs include this context automatically
log.info("Starting checkout");
log.info("Payment processed");4. Log Errors Properly
Include error details and context:
try {
await riskyOperation();
} catch (error) {
ctx.ports.logger.error("Operation failed", {
operation: "riskyOperation",
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
// Add relevant context
userId: ctx.userId,
timestamp: Date.now()
});
}Production Configuration
Recommended settings for production:
LOG_LEVEL=info # Balance between detail and noise
LOG_FORMAT=json # Machine-readable, works with log aggregators
LOG_SERVICE=my-api # Identify your service in logs
LOG_TIMESTAMP=true # Essential for production debuggingDevelopment Configuration
Recommended settings for local development:
LOG_LEVEL=debug # More verbose for debugging
LOG_FORMAT=pretty # Human-readable output
LOG_SERVICE=my-api-dev # Identify as dev environment
LOG_TIMESTAMP=true # Helpful for timing issuesError Handling
The provider will throw errors in these cases:
- Missing required configuration (though all config has defaults)
- Invalid configuration values (validated by Zod schema)
Make sure to handle these during application startup.
License
MIT
