
Nazar Mammedov
Software Engineer
Good Logging Practices: Lessons Learned
4 min read
|
When I Started Coding, I Thought console.log() Was Enough
It worked… until my first real debugging nightmare, hours lost trying to find where things went wrong.
That’s when I realized: logging isn’t optional. It’s one of the most powerful tools for writing quality software.
Most tutorials skip it because they focus on making code work, not making it observable.
But in real projects, poor logging slows you down more than anything else.
Every program has bugs, and we spend most of our time debugging, not building.
A good logging setup changes everything.
Here’s What I’ve Learned 👇
- Study the default logger in detail before reinventing the wheel.
- Build a custom logger, a wrapper around the default logger.
- Add levels (
debug,info,warn,error) and color them. - Show which component or function the log came from, so that you find them easily and fast.
- Make it switchable on/off, so you can disable verbose logs in different environments.
- Allow variable arguments — you’ll need flexibility.
Once I started doing this, my debugging time dropped dramatically.
Good logging keeps your brain focused on solving problems, not chasing ghosts.
Here is an example:
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
NONE = 4
}
export interface LogEntry {
level: LogLevel;
timestamp: Date;
message: string;
source?: string;
data?: any[];
stack?: string;
}
@Injectable({
providedIn: 'root'
})
export class LogService {
// Set minimum log level based on environment
private minLogLevel: LogLevel = environment.production ? LogLevel.WARN : LogLevel.DEBUG;
// Color coding for different log levels
private readonly colors: Record<LogLevel, string> = {
[LogLevel.DEBUG]: '#6c757d', // Gray
[LogLevel.INFO]: '#0dcaf0', // Cyan
[LogLevel.WARN]: '#ffc107', // Yellow
[LogLevel.ERROR]: '#dc3545', // Red
[LogLevel.NONE]: '#000000' // Black (never used)
};
// Icons for different log levels
private readonly icons: Record<LogLevel, string> = {
[LogLevel.DEBUG]: '',
[LogLevel.INFO]: '',
[LogLevel.WARN]: '',
[LogLevel.ERROR]: '',
[LogLevel.NONE]: '' // No icon (never used)
};
constructor() {
this.info('LogService initialized', 'LogService');
}
/**
* Debug level logging with variable arguments
* @example
* log.debug('User loaded', user);
* log.debug('UserComponent', 'User loaded', user, metadata);
*/
debug(sourceOrMessage: string, ...args: any[]): void {
this.logWithArgs(LogLevel.DEBUG, sourceOrMessage, ...args);
}
/**
* Info level logging with variable arguments
* @example
* log.info('Request completed', response);
* log.info('ApiService', 'Request completed', response, timing);
*/
info(sourceOrMessage: string, ...args: any[]): void {
this.logWithArgs(LogLevel.INFO, sourceOrMessage, ...args);
}
/**
* Warning level logging with variable arguments
* @example
* log.warn('Deprecated feature used', feature);
* log.warn('WarningComponent', 'Deprecated feature', feature, alternatives);
*/
warn(sourceOrMessage: string, ...args: any[]): void {
this.logWithArgs(LogLevel.WARN, sourceOrMessage, ...args);
}
/**
* Error level logging with variable arguments
* @example
* log.error('Request failed', error);
* log.error('ApiService', 'Request failed', error, requestData);
*/
error(sourceOrMessage: string, ...args: any[]): void {
const hasError = args.some(arg => arg instanceof Error || (arg && arg.stack));
const stack = hasError
? args.find(arg => arg instanceof Error || (arg && arg.stack))?.stack
: new Error().stack;
this.logWithArgs(LogLevel.ERROR, sourceOrMessage, ...args, stack);
}
/**
* Main logging method with variable arguments support
* Intelligently determines if first arg is source or message
*/
private logWithArgs(level: LogLevel, sourceOrMessage: string, ...args: any[]): void {
let source: string | undefined;
let message: string;
let data: any[] = [];
let stack: string | undefined;
// Check if last arg is a stack trace (for errors)
if (level === LogLevel.ERROR && typeof args[args.length - 1] === 'string' && args[args.length - 1]?.includes('Error:')) {
stack = args.pop();
}
// Determine if first parameter is source or message
// If first arg looks like a message (long string or has spaces) and second arg exists,
// treat first as source
if (args.length > 0 && !sourceOrMessage.includes(' ') && sourceOrMessage.length < 50) {
// First param is likely a source/component name
source = sourceOrMessage;
message = args[0]?.toString() || '';
data = args.slice(1);
} else {
// First param is the message
source = this.getCallerInfo();
message = sourceOrMessage;
data = args;
}
if (level < this.minLogLevel) {
return;
}
const logEntry: LogEntry = {
level,
timestamp: new Date(),
message,
source,
data: data.length > 0 ? data : undefined,
stack
};
this.writeToConsole(logEntry);
}
/**
* Write formatted log to console
*/
private writeToConsole(entry: LogEntry): void {
const levelName = LogLevel[entry.level];
const icon = this.icons[entry.level];
const color = this.colors[entry.level];
const timestamp = this.formatTimestamp(entry.timestamp);
const source = entry.source ? `[${entry.source}]` : '';
// Create styled console output
const styles = [
`color: ${color}`,
'font-weight: bold',
'font-size: 12px'
].join(';');
const sourceStyles = [
'color: #6c757d',
'font-weight: normal',
'font-style: italic'
].join(';');
const messageStyles = [
'color: inherit',
'font-weight: normal'
].join(';');
// Build console arguments
const consoleArgs: any[] = [
`%c${icon} ${levelName} %c${source} %c${timestamp}\n%c${entry.message}`,
styles,
sourceStyles,
'color: #999; font-size: 10px',
messageStyles
];
// Add data if present
if (entry.data && entry.data.length > 0) {
if (entry.data.length === 1) {
consoleArgs.push('\nData:', entry.data[0]);
} else {
consoleArgs.push('\nData:');
entry.data.forEach((item, index) => {
consoleArgs.push(` [${index}]:`, item);
});
}
}
// Add stack trace for errors
if (entry.stack && entry.level === LogLevel.ERROR) {
consoleArgs.push('\nStack:', entry.stack);
}
// Output to console based on level
switch (entry.level) {
case LogLevel.DEBUG:
console.debug(...consoleArgs);
break;
case LogLevel.INFO:
console.info(...consoleArgs);
break;
case LogLevel.WARN:
console.warn(...consoleArgs);
break;
case LogLevel.ERROR:
console.error(...consoleArgs);
break;
}
}
/**
* Format timestamp for display
*/
private formatTimestamp(date: Date): string {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
}
/**
* Attempt to get caller information from stack trace
*/
private getCallerInfo(): string {
try {
const stack = new Error().stack;
if (!stack) return 'Unknown';
const lines = stack.split('\n');
// Find the first line that's not from LogService
for (let i = 3; i < lines.length; i++) {
const line = lines[i];
if (line.includes('LogService')) continue;
// Extract file name and line number
const match = line.match(/at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)/);
if (match) {
const fullPath = match[1];
const fileName = fullPath.split('/').pop()?.replace('.ts', '') || 'Unknown';
const lineNumber = match[2];
return `${fileName}:${lineNumber}`;
}
}
} catch (e) {
// Fallback if stack parsing fails
}
return 'Unknown';
}
/**
* Set minimum log level dynamically
*/
setMinLogLevel(level: LogLevel): void {
this.minLogLevel = level;
this.info(`Log level set to ${LogLevel[level]}`, 'LogService');
}
/**
* Create a logger instance for a specific component
* This logger always prepends the component name
*/
createLogger(componentName: string) {
return {
debug: (message: string, ...data: any[]) => this.debug(componentName, message, ...data),
info: (message: string, ...data: any[]) => this.info(componentName, message, ...data),
warn: (message: string, ...data: any[]) => this.warn(componentName, message, ...data),
error: (message: string, ...data: any[]) => this.error(componentName, message, ...data)
};
}
/**
* Group related logs
*/
group(label: string, source?: string): void {
if (this.minLogLevel <= LogLevel.DEBUG) {
console.group(`📂 ${label} ${source ? `[${source}]` : ''}`);
}
}
/**
* End log group
*/
groupEnd(): void {
if (this.minLogLevel <= LogLevel.DEBUG) {
console.groupEnd();
}
}
/**
* Log a table (useful for arrays of objects)
*/
table(data: any, source?: string): void {
if (this.minLogLevel <= LogLevel.DEBUG) {
this.info('Table data', source || this.getCallerInfo());
console.table(data);
}
}
/**
* Time a block of code
*/
time(label: string): void {
if (this.minLogLevel <= LogLevel.DEBUG) {
console.time(`⏱️ ${label}`);
}
}
/**
* End timing
*/
timeEnd(label: string): void {
if (this.minLogLevel <= LogLevel.DEBUG) {
console.timeEnd(`⏱️ ${label}`);
}
}
/**
* Clear console
*/
clear(): void {
console.clear();
this.info('Console cleared', 'LogService');
}
}
- #Programming
- #Logging
- #Debugging
- #SoftwareDevelopment
- #BestPractices
Hello! How can I help you today?
Virtual Chat- Hello! My name is VirtuBot. I am a virtual assistant representing Nazar. You can ask me questions as if I am Nazar.4:23 PMTell me about yourself?

Powered by NazarAI