Skip to main content

Context and Tags in MagicLogger

This guide covers how to effectively use context and tags in MagicLogger for structured logging, filtering, and log organization.

Overview

Context and Tags are two powerful features that help you add structured metadata to your logs:

  • Context: Structured data (objects) that provide additional information about the log entry
  • Tags: Simple string labels used for categorization and filtering

Context

Context allows you to attach structured data to log entries, providing rich metadata that can be used for debugging, monitoring, and analysis.

Basic Context Usage

import { Logger } from 'magiclogger';

// Global context - applied to all logs from this logger
const logger = new Logger({
id: 'payment-service',
context: {
service: 'payment-api',
version: '2.1.0',
environment: 'production',
region: 'us-east-1'
}
});

// Per-log context - specific to this log entry
logger.info('Payment processed successfully', {
orderId: 'ORD-12345',
customerId: 'CUST-67890',
amount: 99.99,
currency: 'USD',
processingTime: 145,
paymentMethod: 'credit_card'
});

// Tip: When using console-like variadic args, wrap context you don't want
// to print using meta(...):
// import { meta } from 'magiclogger';
// logger.info('Processed', data, meta({ requestId: 'req-1' }));

Context Merging

When both global and per-log context are provided, they are merged with per-log context taking precedence:

const logger = new Logger({
context: { service: 'api', version: '1.0', environment: 'prod' }
});

logger.info('User action', {
userId: '123',
action: 'login',
version: '2.0' // This overrides the global version
});

// Resulting context:
// {
// service: 'api',
// version: '2.0', // Overridden
// environment: 'prod',
// userId: '123',
// action: 'login'
// }

Advanced Context Management

Using the ContextManager for advanced context operations:

import { ContextManager } from 'magiclogger';

const contextManager = new ContextManager({
sensitiveKeys: ['password', 'token', 'ssn'],
transformRules: {
'user.id': 'userId',
'request.id': 'requestId'
}
});

// Merge multiple contexts
const baseContext = { service: 'api', version: '1.0' };
const requestContext = { requestId: 'req-123', userId: '456' };
const merged = contextManager.merge(baseContext, requestContext);

// Sanitize sensitive data
const userContext = {
userId: '123',
email: 'user@example.com',
password: 'secret123'
};
const sanitized = contextManager.sanitize(userContext);
// Result: { userId: '123', email: 'user@example.com', password: '***' }

// Flatten nested context
const nested = {
user: { id: '123', profile: { name: 'John', age: 30 } },
request: { method: 'POST', path: '/api/users' }
};
const flattened = contextManager.flatten(nested);
// Result: {
// 'user.id': '123',
// 'user.profile.name': 'John',
// 'user.profile.age': 30,
// 'request.method': 'POST',
// 'request.path': '/api/users'
// }

Context Validation

Ensure context meets your requirements:

const validation = contextManager.validate(context, {
required: ['userId', 'requestId'],
forbidden: ['password', 'secret'],
maxDepth: 3,
maxSize: 1000 // bytes
});

if (!validation.valid) {
console.error('Context validation failed:', validation.errors);
}

Context Best Practices

  1. Keep it structured: Use consistent field names across your application
  2. Avoid sensitive data: Never log passwords, tokens, or PII without sanitization
  3. Use meaningful keys: Choose descriptive field names that are self-documenting
  4. Limit depth: Avoid deeply nested objects that are hard to query
  5. Consider size: Large context objects can impact performance
// Good context structure
const goodContext = {
userId: '123',
requestId: 'req-456',
operation: 'user.update',
duration: 145,
success: true
};

// Avoid this
const badContext = {
u: '123', // Unclear abbreviation
data: { // Too generic
stuff: {
things: {
deep: {
nested: 'value' // Too deep
}
}
}
},
password: 'secret123' // Sensitive data
};

Tags

Tags are simple string labels that help categorize and filter log entries. They're perfect for grouping related logs and enabling efficient filtering.

Basic Tag Usage

// Global tags - applied to all logs from this logger
const logger = new Logger({
id: 'api-service',
tags: ['api', 'production', 'v2']
});

// Tags are automatically included in all log entries
logger.info('Server started');
logger.error('Database connection failed');

Hierarchical Tags

MagicLogger implements a sophisticated hierarchical tag system that supports both implicit (dot notation) and explicit (parent-child) hierarchies:

1. Dot Notation Hierarchy (Implicit)

The most common approach uses dot notation to create implicit hierarchies:

// Tags automatically form a hierarchy
logger.info('Database query executed', {
tags: ['database.query.select', 'performance.slow']
});

// These tags implicitly match at multiple levels:
// - 'database' (parent)
// - 'database.query' (sub-parent)
// - 'database.query.select' (specific)

2. Explicit Parent-Child Relationships

The TagManager also supports explicit hierarchy definitions:

import { TagManager } from 'magiclogger';

const tagManager = new TagManager();

// Define explicit parent-child relationships
tagManager.setHierarchy('api', ['api.v1', 'api.v2', 'api.internal']);
tagManager.setHierarchy('api.v1', ['api.v1.users', 'api.v1.posts']);

// Query the hierarchy
tagManager.getChildren('api'); // ['api.v1', 'api.v2', 'api.internal']
tagManager.getParents('api.v1.users'); // ['api', 'api.v1']

// Expand to include full hierarchy
tagManager.expandHierarchy('api.v1', true, true);
// Returns: ['api.v1', 'api', 'api.v1.users', 'api.v1.posts']

3. Path-Based Tag Generation

Generate hierarchical tags from file paths:

// Generate hierarchical tags from file paths
const tags = tagManager.fromPath('src/api/v2/users/create.ts');
// Result: ['src', 'src.api', 'src.api.v2', 'src.api.v2.users', 'src.api.v2.users.create']

// Group related functionality
const tags = [
'service.api',
'service.auth',
'database.read',
'database.write',
'cache.redis',
'cache.memory'
];

const grouped = tagManager.group(tags);
// Result: {
// service: ['api', 'auth'],
// database: ['read', 'write'],
// cache: ['redis', 'memory']
// }

4. Hierarchical Transport Filtering

Configure transports to filter by hierarchical tags:

// Configure transports to filter by hierarchical tags
new FileTransport({
filepath: './api.log',
tags: ['api'], // Catches all api.* tags
excludeTags: ['api.internal'] // But excludes internal API logs
});

// Custom filter logic for hierarchical matching
const apiTransport = new FileTransport({
filepath: './api.log',
tagFilter: (tags) => {
// Custom filter logic for hierarchical matching
return tags.some(tag => tag.startsWith('api.'));
}
});

5. Theme Selection by Tag Hierarchy

More specific tags override general ones in theming:

const logger = new Logger({
theme: {
tags: {
'api': ['cyan', 'bold'], // All API logs
'api.request': ['cyan'], // API requests specifically
'api.response': ['brightCyan'], // API responses
'api.error': ['red', 'bold'], // API errors
'database': ['yellow'], // All database logs
'database.slow': ['yellow', 'bold', 'bgRed'] // Slow queries highlighted
}
}
});

// Hierarchical matching - more specific tags override general ones
logger.info('Slow query detected', { tags: ['database.slow'] });
// Gets the 'database.slow' theme, not just 'database'

6. Programmatic Pattern Matching

Check if tags match patterns with wildcards:

const tags = ['api.v1.users', 'production', 'slow'];

// Check if tags match patterns
tagManager.matches(tags, {
any: ['api.*', 'database.*'] // Matches anything under api or database
}); // true

tagManager.matches(tags, {
all: ['api.*', 'production'], // Must match api hierarchy AND production
none: ['*.internal', 'debug'] // Must not match internal or debug
}); // true

Practical Example: Microservice Logging

// Set up hierarchical tags for a microservice
const logger = new Logger({
tags: ['service.api.v2']
});

// This log entry will be caught by transports filtering for:
// - 'service' (parent)
// - 'service.api' (sub-parent)
// - 'service.api.v2' (exact)
logger.info('Request processed', {
tags: ['endpoint.users.create'] // Additional hierarchical tag
});

// Transport sees combined tags and can filter hierarchically
const serviceTransport = new FileTransport({
filepath: './service.log',
tagFilter: (tags) => {
// Capture all service logs
return tags.some(tag => tag.startsWith('service.'));
}
});

const apiTransport = new FileTransport({
filepath: './api-only.log',
tagFilter: (tags) => {
// Only API-specific logs
return tags.some(tag => tag.startsWith('service.api.'));
}
});

Tag Normalization

Ensure consistent tag formatting:

const tagManager = new TagManager({
normalizationRules: {
lowercase: true,
replaceSpaces: true,
replaceDots: true,
maxLength: 30
}
});

const rawTags = ['API Service', 'v1.0', 'PRODUCTION', 'user@auth'];
const normalized = tagManager.normalize(rawTags);
// Result: ['api-service', 'v1-0', 'production', 'user-auth']

Tag Filtering and Matching

Filter logs based on tags:

// Transport-level tag filtering
new FileTransport({
name: 'api-logs',
filepath: './api.log',
tags: ['api'], // Only logs with 'api' tag
excludeTags: ['debug'] // Exclude debug logs
});

// Programmatic tag matching
const tags = ['api', 'v1', 'production'];

const hasRequired = tagManager.matches(tags, {
all: ['api', 'production'] // Must have both
}); // true

const hasAny = tagManager.matches(tags, {
any: ['debug', 'v1'] // Must have at least one
}); // true

const noForbidden = tagManager.matches(tags, {
none: ['debug', 'test'] // Must not have any
}); // true

Dynamic Tag Generation

Generate tags from objects or runtime data:

const requestInfo = {
method: 'POST',
path: '/api/users',
status: 200,
authenticated: true
};

const tags = tagManager.fromObject(requestInfo, {
prefix: 'http',
includeArrays: true
});
// Result: ['http-method-post', 'http-path-api-users', 'http-status-200', 'http-authenticated']

Tag Validation

Ensure tags meet your standards:

const validation = tagManager.validate(['api', 'v1', 'production'], {
maxCount: 5,
required: ['api'],
forbidden: ['debug', 'test'],
allowedPatterns: [/^[a-z0-9-]+$/]
});

if (!validation.valid) {
console.error('Tag validation failed:', validation.errors);
}

Tag-Driven Theme Selection

Tags can automatically select a theme when no explicit theme is set:

// Map specific tags to theme names
const logger1 = new Logger({
tags: ['acme'],
themeByTag: { acme: 'cyberpunk', contoso: 'dark' }
});

// Or rely on implicit matching: if a tag equals a named theme
const logger2 = new Logger({ tags: ['neon'] }); // loads the 'neon' theme if available

// Changing tags can re-select the theme (when not explicitly set)
logger2.updateConfig({ tags: ['dark'] });

Notes

  • An explicit theme object/string overrides auto-selection.
  • Auto-selection prefers themeByTag mappings, then tag-name matches.

Transport Integration

Both context and tags integrate seamlessly with transports for filtering and formatting.

Filtering by Tags

// Only send errors to Slack
new SlackTransport({
webhook: process.env.SLACK_WEBHOOK,
tags: ['error', 'critical']
});

// Audit logs to secure storage
new S3Transport({
bucket: 'audit-logs',
tags: ['audit', 'compliance'],
excludeTags: ['debug', 'test']
});

// Development logs to console
new ConsoleTransport({
tags: ['development'],
showTags: true,
showMetadata: true
});

Context in Formatters

// Custom formatter that includes specific context fields
new FileTransport({
filepath: './app.log',
formatter: (entry) => {
const timestamp = entry.timestamp;
const level = entry.level.toUpperCase();
const message = entry.plainMessage || entry.message;
const userId = entry.context?.userId || 'anonymous';
const requestId = entry.context?.requestId || 'no-request';

return `[${timestamp}] ${level} [${userId}] [${requestId}] ${message}\n`;
}
});

// Use built-in formatters with context support
new FileTransport({
filepath: './structured.log',
formatter: Formatters.json.pretty // Includes full context
});

Real-World Examples

Express.js Integration

import express from 'express';
import { Logger, ContextManager, TagManager } from 'magiclogger';

const app = express();
const contextManager = new ContextManager();
const tagManager = new TagManager();

// Request logging middleware
app.use((req, res, next) => {
const requestId = generateRequestId();
const startTime = Date.now();

// Create request-specific logger
req.logger = new Logger({
context: {
requestId,
method: req.method,
path: req.path,
userAgent: req.get('User-Agent'),
ip: req.ip
},
tags: tagManager.fromPath(`http/${req.method.toLowerCase()}${req.path}`)
});

// Log request start
req.logger.info('Request started');

// Log response
res.on('finish', () => {
const duration = Date.now() - startTime;
req.logger.info('Request completed', {
status: res.statusCode,
duration,
contentLength: res.get('Content-Length')
});
});

next();
});

// Route handler
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;

try {
// Add user context
const userContext = { userId, operation: 'user.fetch' };
const mergedContext = contextManager.merge(req.logger.context, userContext);

req.logger.info('Fetching user', userContext);

const user = await getUserById(userId);

req.logger.info('User fetched successfully', {
userId,
userExists: !!user
});

res.json(user);
} catch (error) {
req.logger.error('Failed to fetch user', {
userId,
error: error.message,
stack: error.stack
});

res.status(500).json({ error: 'Internal server error' });
}
});

Microservice Correlation

// Service A
const serviceALogger = new Logger({
context: {
service: 'user-service',
version: '1.2.0',
instance: process.env.INSTANCE_ID
},
tags: ['user-service', 'microservice']
});

async function processUserRequest(correlationId: string, userId: string) {
const requestContext = {
correlationId,
userId,
operation: 'user.process'
};

serviceALogger.info('Processing user request', requestContext);

// Call Service B
const result = await callServiceB(correlationId, userId);

serviceALogger.info('User request completed', {
...requestContext,
success: true,
result: result.id
});
}

// Service B
const serviceBLogger = new Logger({
context: {
service: 'notification-service',
version: '2.0.1',
instance: process.env.INSTANCE_ID
},
tags: ['notification-service', 'microservice']
});

async function callServiceB(correlationId: string, userId: string) {
const requestContext = {
correlationId,
userId,
operation: 'notification.send'
};

serviceBLogger.info('Received request from user-service', requestContext);

// Process notification
const notification = await sendNotification(userId);

serviceBLogger.info('Notification sent', {
...requestContext,
notificationId: notification.id,
channel: notification.channel
});

return notification;
}

Error Tracking and Alerting

const errorLogger = new Logger({
context: {
service: 'payment-processor',
environment: process.env.NODE_ENV
},
tags: ['payment', 'critical'],
transports: [
// Console for development
new ConsoleTransport({
level: 'debug',
showTags: true,
showMetadata: true
}),

// File for all logs
new FileTransport({
filepath: './logs/payment.log',
formatter: Formatters.json.compact
}),

// Slack for errors
new SlackTransport({
webhook: process.env.SLACK_WEBHOOK,
levels: ['error'],
formatter: (entry) => {
const context = entry.context || {};
return {
text: `🚨 Payment Error: ${entry.message}`,
attachments: [{
color: 'danger',
fields: [
{ title: 'Service', value: context.service, short: true },
{ title: 'Environment', value: context.environment, short: true },
{ title: 'Order ID', value: context.orderId, short: true },
{ title: 'Customer ID', value: context.customerId, short: true },
{ title: 'Error', value: entry.error?.message, short: false }
]
}]
};
}
})
]
});

async function processPayment(order: Order) {
const paymentContext = {
orderId: order.id,
customerId: order.customerId,
amount: order.amount,
currency: order.currency,
paymentMethod: order.paymentMethod
};

errorLogger.info('Processing payment', paymentContext);

try {
const result = await chargePayment(order);

errorLogger.info('Payment successful', {
...paymentContext,
transactionId: result.transactionId,
processingTime: result.processingTime
});

return result;
} catch (error) {
errorLogger.error('Payment failed', {
...paymentContext,
error,
attemptNumber: order.attemptNumber || 1,
lastAttempt: new Date().toISOString()
});

throw error;
}
}

Performance Considerations

  1. Context Size: Large context objects are serialized for each log entry. Keep them reasonably sized.

  2. Tag Normalization: Tag normalization happens at logger creation time, not per log entry.

  3. Memory Usage: Context and tags are stored in memory until the log entry is processed.

  4. Serialization: JSON serialization of context happens when formatting logs for output.

// Efficient context usage
const logger = new Logger({
context: {
service: 'api',
version: '1.0'
}
});

// Avoid large objects in per-log context
logger.info('User action', {
userId: '123',
action: 'login'
// Don't include: largeUserObject, fullRequestBody, etc.
});

// Use context snapshots for debugging
const contextSnapshot = contextManager.snapshot(largeContext);
logger.debug('Context snapshot created', {
snapshotId: contextSnapshot.timestamp,
size: contextSnapshot.size,
depth: contextSnapshot.depth
});

Migration Guide

If you're migrating from other logging libraries:

From Winston

// Winston
const winston = require('winston');
const logger = winston.createLogger({
defaultMeta: { service: 'user-service' }
});

logger.info('Hello world', { userId: '123' });

// MagicLogger equivalent
const logger = new Logger({
context: { service: 'user-service' }
});

logger.info('Hello world', { userId: '123' });

From Bunyan

// Bunyan
const bunyan = require('bunyan');
const logger = bunyan.createLogger({
name: 'myapp',
service: 'user-service'
});

logger.info({ userId: '123' }, 'Hello world');

// MagicLogger equivalent
const logger = new Logger({
id: 'myapp',
context: { service: 'user-service' }
});

logger.info('Hello world', { userId: '123' });

Conclusion

Context and tags are powerful features that enable structured, searchable, and well-organized logging. Use context for rich metadata and tags for categorization and filtering. The new ContextManager and TagManager components provide advanced functionality for complex logging scenarios.

For more information, see: