Logger

martini-kit’s Logger provides structured, channel-based logging inspired by Unity’s Debug class. It supports log levels, contexts, performance timing, and DevTools integration.

Quick Start

import { Logger } from '@martini-kit/core';

// Create a logger for your module
const gameLogger = new Logger('Game');
const physicsLogger = new Logger('Physics');

gameLogger.log('Player joined', playerId);
gameLogger.warn('Low health', { health: 20 });
gameLogger.error('Invalid state', state);

physicsLogger.log('Collision detected', body1, body2);

API Reference

class Logger {
  constructor(channel?: string, parentContext?: Record<string, any>);

  // Logging methods
  log(message: string, ...data: any[]): void;
  warn(message: string, ...data: any[]): void;
  error(message: string, ...data: any[]): void;

  // Grouping
  group(label: string): void;
  groupEnd(): void;

  // Assertions
  assert(condition: boolean, message?: string): void;

  // Performance timing
  time(label: string): void;
  timeEnd(label: string): void;

  // Child loggers
  channel(name: string): Logger;

  // Configuration
  setEnabled(enabled: boolean): void;
  setMinLevel(level: LogLevel): void;
  setContext(context: Record<string, any> | undefined): void;
  setIncludeStack(include: boolean): void;

  // Listeners (for DevTools)
  onLog(listener: LogListener): () => void;
}

// Types
type LogLevel = 'log' | 'warn' | 'error';
type LogListener = (entry: LogEntry) => void;

interface LogEntry {
  level: LogLevel;
  channel: string;
  message: string;
  data: any[];
  timestamp: number;
  context?: Record<string, any>;
  stack?: string;
}

Basic Usage

Creating Loggers

import { Logger } from '@martini-kit/core';

// Simple logger
const logger = new Logger('MyGame');

// Channel-based organization
const networkLogger = new Logger('Network');
const audioLogger = new Logger('Audio');
const uiLogger = new Logger('UI');

Logging Messages

// Info
logger.log('Game started');
logger.log('Player joined', { playerId: 'p1', name: 'Alice' });

// Warning
logger.warn('Low FPS detected', { fps: 15 });

// Error
logger.error('Failed to load asset', { path: 'sprites/player.png' });

Console output:

[MyGame] Game started
[MyGame] Player joined { playerId: 'p1', name: 'Alice' }
⚠️ [MyGame] Low FPS detected { fps: 15 }
❌ [MyGame] Failed to load asset { path: 'sprites/player.png' }

Child Loggers

Create nested loggers for better organization:

const gameLogger = new Logger('Game');
const playerLogger = gameLogger.channel('Player');
const enemyLogger = gameLogger.channel('Enemy');

playerLogger.log('Health changed', 75);  // [Game:Player] Health changed 75
enemyLogger.log('Spawned', { type: 'orc' });  // [Game:Enemy] Spawned {type: 'orc'}

Deep nesting:

const game = new Logger('Game');
const physics = game.channel('Physics');
const collision = physics.channel('Collision');

collision.log('Box hit wall');  // [Game:Physics:Collision] Box hit wall

Log Levels

Filter logs by minimum level:

const logger = new Logger('Game');

// Show all logs (default)
logger.setMinLevel('log');

// Only warnings and errors
logger.setMinLevel('warn');

// Only errors
logger.setMinLevel('error');

logger.log('Debug info');      // Hidden if minLevel is 'warn' or 'error'
logger.warn('Warning');         // Hidden if minLevel is 'error'
logger.error('Critical error'); // Always shown

Priority order: log (lowest) < warn < error (highest)

Context

Attach metadata to all log entries:

const logger = new Logger('Game');

// Add context
logger.setContext({ sessionId: 'abc123', playerId: 'p1' });

logger.log('Action performed');
// Log entry includes: { sessionId: 'abc123', playerId: 'p1' }

// Update context
logger.setContext({ sessionId: 'abc123', playerId: 'p1', level: 5 });

// Clear context
logger.setContext(undefined);

Inherited context:

const parent = new Logger('Game');
parent.setContext({ sessionId: 'abc123' });

const child = parent.channel('Physics');
child.setContext({ engine: 'Arcade' });

child.log('Collision');
// Context: { sessionId: 'abc123', engine: 'Arcade' }

Grouping

Create collapsible log groups:

logger.group('Game Loop');
logger.log('Update physics');
logger.log('Check collisions');
logger.log('Render frame');
logger.groupEnd();

logger.group('Player Actions');
logger.log('Move');
logger.log('Jump');
logger.groupEnd();

Console output:

▼ [Game] Game Loop
    [Game] Update physics
    [Game] Check collisions
    [Game] Render frame
▼ [Game] Player Actions
    [Game] Move
    [Game] Jump

Assertions

Log errors when conditions fail:

logger.assert(health > 0, 'Health must be positive');
logger.assert(players.length > 0, 'No players in game');

// Custom assertions
function assertPlayer(player: Player | undefined): asserts player is Player {
  logger.assert(player !== undefined, 'Player is undefined');
  if (!player) throw new Error('Player not found');
}

Performance Timing

Measure execution time:

logger.time('loadAssets');
await loadAllAssets();
logger.timeEnd('loadAssets');
// Output: [Game] loadAssets: 1234.56ms

logger.time('physics');
updatePhysics();
logger.timeEnd('physics');
// Output: [Game] physics: 3.21ms

Nested timers:

logger.time('gameLoop');

logger.time('physics');
updatePhysics();
logger.timeEnd('physics');

logger.time('rendering');
renderFrame();
logger.timeEnd('rendering');

logger.timeEnd('gameLoop');

Integration with DevTools

Listen for log entries (used by martini-kit DevTools):

const logger = new Logger('Game');

const unsubscribe = logger.onLog((entry) => {
  console.log('Log entry:', entry);
  // {
  //   level: 'log',
  //   channel: 'Game',
  //   message: 'Player joined',
  //   data: ['p1'],
  //   timestamp: 1234567890,
  //   context: { sessionId: 'abc123' }
  // }

  // Send to DevTools panel
  sendToDevTools(entry);
});

// Later: stop listening
unsubscribe();

Filtering:

logger.onLog((entry) => {
  // Only errors
  if (entry.level === 'error') {
    reportToErrorTracking(entry);
  }

  // Only specific channel
  if (entry.channel.startsWith('Network')) {
    logToNetworkMonitor(entry);
  }
});

Configuration

Enable/Disable Console Output

const logger = new Logger('Game');

// Disable console output (listeners still notified)
logger.setEnabled(false);

logger.log('Hidden in console');  // Not shown, but listeners still get it

// Re-enable
logger.setEnabled(true);

Use case: Disable console spam in production while keeping DevTools integration.

Include Stack Traces

logger.setIncludeStack(true);

logger.log('Something happened');
// Log entry includes 'stack' property with call stack

Automatic for errors:

logger.error('Critical error');
// Stack trace automatically included (even if setIncludeStack is false)

Common Patterns

Module-Scoped Loggers

// player.ts
const logger = new Logger('Player');

export function updatePlayer(player: Player) {
  logger.log('Updating player', player.id);
  // ...
}

// enemy.ts
const logger = new Logger('Enemy');

export function spawnEnemy(type: string) {
  logger.log('Spawning enemy', type);
  // ...
}

Conditional Logging

const DEV = process.env.NODE_ENV !== 'production';
const logger = new Logger('Game');

if (DEV) {
  logger.log('Debug info', debugData);
}

// Or use minLevel in production
if (!DEV) {
  logger.setMinLevel('error');  // Only errors in production
}

Logging in Actions

import { defineGame } from '@martini-kit/core';

const logger = new Logger('GameActions');

export const game = defineGame({
  actions: {
    move: {
      apply(state, context, input) {
        logger.log('Move action', { playerId: context.targetId, input });

        const player = state.players[context.targetId];
        if (!player) {
          logger.warn('Player not found', context.targetId);
          return;
        }

        player.x = input.x;
        player.y = input.y;
      }
    }
  }
});

Performance Monitoring

const perfLogger = new Logger('Performance');

// Track action execution time
actions: {
  tick: {
    apply(state, context) {
      perfLogger.time('tick');

      perfLogger.time('physics');
      updatePhysics(state);
      perfLogger.timeEnd('physics');

      perfLogger.time('ai');
      updateAI(state);
      perfLogger.timeEnd('ai');

      perfLogger.timeEnd('tick');
    }
  }
}

Default Logger

martini-kit exports a default logger instance:

import { logger } from '@martini-kit/core';

logger.log('Using default martini-kit logger');  // [martini-kit] Using default martini-kit logger

Create your own:

// Prefer creating your own for better channel organization
const myLogger = new Logger('MyGame');

Complete Example

import { Logger, defineGame } from '@martini-kit/core';

// Create channel-based loggers
const gameLogger = new Logger('Game');
const physicsLogger = gameLogger.channel('Physics');
const networkLogger = gameLogger.channel('Network');

// Configure
if (process.env.NODE_ENV === 'production') {
  gameLogger.setMinLevel('error');
}

gameLogger.setContext({ version: '1.0.0' });

// Use in game definition
export const game = defineGame({
  setup: ({ playerIds }) => {
    gameLogger.log('Game setup', { playerCount: playerIds.length });

    return {
      players: Object.fromEntries(
        playerIds.map(id => {
          gameLogger.log('Initializing player', id);
          return [id, { x: 100, y: 100, health: 100 }];
        })
      )
    };
  },

  actions: {
    move: {
      apply(state, context, input) {
        physicsLogger.log('Move', { player: context.targetId, input });

        const player = state.players[context.targetId];
        if (!player) {
          gameLogger.error('Player not found', context.targetId);
          return;
        }

        player.x = input.x;
        player.y = input.y;
      }
    }
  },

  onPlayerJoin(state, playerId) {
    networkLogger.log('Player joined', playerId);
    gameLogger.assert(state.players[playerId] === undefined, 'Player already exists');
  },

  onPlayerLeave(state, playerId) {
    networkLogger.warn('Player left', playerId);
  }
});

// DevTools integration
gameLogger.onLog((entry) => {
  if (entry.level === 'error') {
    // Report to error tracking service
    reportError(entry);
  }
});

Best Practices

✅ Do

  • Create channel-based loggers - Organize by module/feature
  • Use appropriate log levels - log for info, warn for issues, error for critical
  • Add context - Include session/player IDs for debugging
  • Use timers - Measure performance-critical sections
  • Filter in production - Set minLevel('error') in production

❌ Don’t

  • Don’t overuse logging - Too many logs = noise
  • Don’t log sensitive data - Passwords, tokens, etc.
  • Don’t log in hot loops - Performance impact
  • Don’t rely on console in production - Use proper monitoring tools
  • Don’t forget to clean up listeners - Call the unsubscribe function

See Also