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: