Skip to main content

Logging

Effective logging is essential for debugging, monitoring, and understanding your application's behavior in production.

Introduction​

Rikta doesn't prescribe a specific logging solution, giving you the flexibility to use your preferred logging library. This guide covers best practices and common patterns.

Basic Logging​

Console Logging​

For simple applications:

import { Injectable } from '@riktajs/core';

@Injectable()
export class AppService {
doSomething() {
console.log('Something happened');
console.error('An error occurred');
console.warn('Warning message');
}
}

Custom Logger Service​

Create a structured logger:

import { Injectable } from '@riktajs/core';

type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface LogEntry {
level: LogLevel;
message: string;
timestamp: string;
context?: string;
data?: unknown;
}

@Injectable()
export class LoggerService {
private context: string = 'Application';

setContext(context: string) {
this.context = context;
return this;
}

debug(message: string, data?: unknown) {
this.log('debug', message, data);
}

info(message: string, data?: unknown) {
this.log('info', message, data);
}

warn(message: string, data?: unknown) {
this.log('warn', message, data);
}

error(message: string, data?: unknown) {
this.log('error', message, data);
}

private log(level: LogLevel, message: string, data?: unknown) {
const entry: LogEntry = {
level,
message,
timestamp: new Date().toISOString(),
context: this.context,
data,
};

const output = JSON.stringify(entry);

switch (level) {
case 'error':
console.error(output);
break;
case 'warn':
console.warn(output);
break;
default:
console.log(output);
}
}
}

Using the Logger​

In Services​

import { Injectable, Autowired } from '@riktajs/core';

@Injectable()
export class UserService {
@Autowired()
private logger!: LoggerService;

async createUser(data: CreateUserDto) {
this.logger.setContext('UserService');
this.logger.info('Creating user', { email: data.email });

try {
const user = await this.database.insert(data);
this.logger.info('User created', { userId: user.id });
return user;
} catch (error) {
this.logger.error('Failed to create user', { error, data });
throw error;
}
}
}

In Controllers​

@Controller('/users')
export class UserController {
@Autowired()
private logger!: LoggerService;

@Post()
async create(@Body() data: CreateUserDto, @Req() request: FastifyRequest) {
this.logger.info('Create user request', {
ip: request.ip,
userAgent: request.headers['user-agent'],
});

return this.userService.create(data);
}
}

Using Pino​

Pino is a fast logger that works great with Fastify.

Setup​

npm install pino pino-pretty

Logger Service with Pino​

import { Injectable } from '@riktajs/core';
import pino, { Logger } from 'pino';

@Injectable()
export class PinoLoggerService {
private logger: Logger;

constructor() {
this.logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
});
}

child(bindings: Record<string, unknown>) {
return this.logger.child(bindings);
}

info(msg: string, data?: object) {
this.logger.info(data, msg);
}

error(msg: string, data?: object) {
this.logger.error(data, msg);
}

warn(msg: string, data?: object) {
this.logger.warn(data, msg);
}

debug(msg: string, data?: object) {
this.logger.debug(data, msg);
}
}

Fastify Integration​

import { Rikta } from '@riktajs/core';
import pino from 'pino';

async function bootstrap() {
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
});

const app = await Rikta.create({
port: 3000,
autowired: ['./src'],
fastifyOptions: {
logger, // Fastify uses pino natively
},
});

await app.listen();
}

Request Logging​

Request Logger Middleware​

import { Injectable } from '@riktajs/core';
import { FastifyRequest, FastifyReply } from 'fastify';

@Injectable()
export class RequestLoggerService {
@Autowired()
private logger!: LoggerService;

logRequest(request: FastifyRequest, reply: FastifyReply, done: () => void) {
const start = Date.now();

reply.addHook('onSend', (req, rep, payload, done) => {
const duration = Date.now() - start;

this.logger.info('Request completed', {
method: request.method,
url: request.url,
statusCode: reply.statusCode,
duration: `${duration}ms`,
ip: request.ip,
userAgent: request.headers['user-agent'],
});

done();
});

done();
}
}

Correlation IDs​

Track requests across services:

import { Injectable } from '@riktajs/core';
import { randomUUID } from 'crypto';

@Injectable()
export class CorrelationService {
private correlationId: string = '';

setCorrelationId(id?: string) {
this.correlationId = id || randomUUID();
}

getCorrelationId(): string {
return this.correlationId;
}
}

// In your request handling
@Injectable()
export class LoggerService {
@Autowired()
private correlation!: CorrelationService;

info(message: string, data?: object) {
console.log(JSON.stringify({
correlationId: this.correlation.getCorrelationId(),
message,
...data,
}));
}
}

Log Levels​

Configure different log levels per environment:

import { Injectable, AbstractConfigProvider, ConfigProperty } from '@riktajs/core';

export class LogConfig extends AbstractConfigProvider<{ level: string }> {
@ConfigProperty({ env: 'LOG_LEVEL' })
level: 'debug' | 'info' | 'warn' | 'error' = 'info';
}

@Injectable()
export class LoggerService {
@Autowired()
private config!: LogConfig;

private levels = ['debug', 'info', 'warn', 'error'];

private shouldLog(level: string): boolean {
return this.levels.indexOf(level) >= this.levels.indexOf(this.config.level);
}

debug(message: string, data?: object) {
if (this.shouldLog('debug')) {
this.output('debug', message, data);
}
}

info(message: string, data?: object) {
if (this.shouldLog('info')) {
this.output('info', message, data);
}
}

// ... etc
}

Structured Logging​

Output logs as JSON for easy parsing:

interface LogEntry {
timestamp: string;
level: string;
service: string;
environment: string;
correlationId?: string;
message: string;
data?: unknown;
error?: {
name: string;
message: string;
stack?: string;
};
}

@Injectable()
export class StructuredLogger {
@Autowired()
private config!: AppConfigProvider;

error(message: string, error?: Error, data?: object) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level: 'error',
service: this.config.serviceName,
environment: this.config.environment,
message,
data,
};

if (error) {
entry.error = {
name: error.name,
message: error.message,
stack: error.stack,
};
}

console.error(JSON.stringify(entry));
}
}

Sensitive Data​

Redacting Secrets​

@Injectable()
export class SafeLogger {
private sensitiveFields = ['password', 'token', 'secret', 'authorization'];

log(message: string, data?: object) {
const safeData = this.redact(data);
console.log(JSON.stringify({ message, data: safeData }));
}

private redact(obj: unknown): unknown {
if (!obj || typeof obj !== 'object') return obj;

const result: Record<string, unknown> = {};

for (const [key, value] of Object.entries(obj)) {
if (this.sensitiveFields.some(f => key.toLowerCase().includes(f))) {
result[key] = '[REDACTED]';
} else if (typeof value === 'object') {
result[key] = this.redact(value);
} else {
result[key] = value;
}
}

return result;
}
}

Best Practices​

1. Use Structured Logging​

// ✅ Good - structured, parseable
this.logger.info('User created', { userId: user.id, email: user.email });

// ❌ Avoid - hard to parse
this.logger.info(`User ${user.id} with email ${user.email} created`);

2. Include Context​

// ✅ Good - enough context to debug
this.logger.error('Database query failed', {
query: 'SELECT * FROM users WHERE id = ?',
params: [userId],
error: error.message,
duration: '150ms',
});

// ❌ Avoid - not enough context
this.logger.error('Query failed');

3. Use Appropriate Levels​

this.logger.debug('Entering method X');        // Development only
this.logger.info('User logged in', { userId }); // Normal operations
this.logger.warn('Rate limit approaching'); // Potential issues
this.logger.error('Payment failed', { error }); // Errors

4. Don't Log Sensitive Data​

// ✅ Good - password not logged
this.logger.info('Login attempt', { email: user.email });

// ❌ Bad - logging password
this.logger.info('Login attempt', { email, password });

5. Log at Boundaries​

// Log at entry and exit of important operations
async processPayment(orderId: string) {
this.logger.info('Processing payment', { orderId });

try {
const result = await this.paymentService.charge(orderId);
this.logger.info('Payment successful', { orderId, transactionId: result.id });
return result;
} catch (error) {
this.logger.error('Payment failed', { orderId, error });
throw error;
}
}