chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@sim/logger",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"bun": ">=1.2.13",
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "biome check --write --unsafe .",
|
||||
"lint:check": "biome check .",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome format .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sim/utils": "workspace:*",
|
||||
"chalk": "5.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sim/tsconfig": "workspace:*",
|
||||
"@types/node": "24.2.1",
|
||||
"typescript": "^7.0.2",
|
||||
"vitest": "^4.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import { createLogger, Logger, LogLevel } from './index'
|
||||
|
||||
/**
|
||||
* Tests for the console logger module.
|
||||
* Tests the Logger class and createLogger factory function.
|
||||
*/
|
||||
|
||||
describe('Logger', () => {
|
||||
let consoleLogSpy: ReturnType<typeof vi.spyOn>
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
consoleLogSpy.mockRestore()
|
||||
consoleErrorSpy.mockRestore()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('class instantiation', () => {
|
||||
test('should create logger instance with module name', () => {
|
||||
const logger = new Logger('TestModule')
|
||||
expect(logger).toBeDefined()
|
||||
expect(logger).toBeInstanceOf(Logger)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createLogger factory', () => {
|
||||
test('should create logger instance with expected methods', () => {
|
||||
const logger = createLogger('MyComponent')
|
||||
expect(logger).toBeDefined()
|
||||
expect(typeof logger.debug).toBe('function')
|
||||
expect(typeof logger.info).toBe('function')
|
||||
expect(typeof logger.warn).toBe('function')
|
||||
expect(typeof logger.error).toBe('function')
|
||||
})
|
||||
|
||||
test('should create multiple independent loggers', () => {
|
||||
const logger1 = createLogger('Component1')
|
||||
const logger2 = createLogger('Component2')
|
||||
expect(logger1).not.toBe(logger2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LogLevel enum', () => {
|
||||
test('should have correct log levels', () => {
|
||||
expect(LogLevel.DEBUG).toBe('DEBUG')
|
||||
expect(LogLevel.INFO).toBe('INFO')
|
||||
expect(LogLevel.WARN).toBe('WARN')
|
||||
expect(LogLevel.ERROR).toBe('ERROR')
|
||||
})
|
||||
})
|
||||
|
||||
describe('logging methods', () => {
|
||||
test('should have debug method', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
expect(typeof logger.debug).toBe('function')
|
||||
})
|
||||
|
||||
test('should have info method', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
expect(typeof logger.info).toBe('function')
|
||||
})
|
||||
|
||||
test('should have warn method', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
expect(typeof logger.warn).toBe('function')
|
||||
})
|
||||
|
||||
test('should have error method', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
expect(typeof logger.error).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('logging behavior', () => {
|
||||
test('should not throw when calling debug', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
expect(() => logger.debug('Test debug message')).not.toThrow()
|
||||
})
|
||||
|
||||
test('should not throw when calling info', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
expect(() => logger.info('Test info message')).not.toThrow()
|
||||
})
|
||||
|
||||
test('should not throw when calling warn', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
expect(() => logger.warn('Test warn message')).not.toThrow()
|
||||
})
|
||||
|
||||
test('should not throw when calling error', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
expect(() => logger.error('Test error message')).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('object formatting', () => {
|
||||
test('should handle null and undefined arguments', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
|
||||
expect(() => {
|
||||
logger.info('Message with null:', null)
|
||||
logger.info('Message with undefined:', undefined)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
test('should handle object arguments', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
const testObj = { key: 'value', nested: { data: 123 } }
|
||||
|
||||
expect(() => {
|
||||
logger.info('Message with object:', testObj)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
test('should handle Error objects', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
const testError = new Error('Test error message')
|
||||
|
||||
expect(() => {
|
||||
logger.error('An error occurred:', testError)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
test('should handle circular references gracefully', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
const circularObj: Record<string, unknown> = { name: 'test' }
|
||||
circularObj.self = circularObj
|
||||
|
||||
expect(() => {
|
||||
logger.info('Circular object:', circularObj)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
test('should handle arrays', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
const testArray = [1, 2, 3, { nested: true }]
|
||||
|
||||
expect(() => {
|
||||
logger.info('Array data:', testArray)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
test('should handle multiple arguments', () => {
|
||||
const logger = createLogger('TestModule')
|
||||
|
||||
expect(() => {
|
||||
logger.debug('Multiple args:', 'string', 123, { obj: true }, ['array'])
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('withMetadata', () => {
|
||||
const createEnabledLogger = () =>
|
||||
new Logger('Test', { enabled: true, colorize: false, logLevel: LogLevel.DEBUG })
|
||||
|
||||
test('should return a new Logger instance', () => {
|
||||
const logger = createEnabledLogger()
|
||||
const child = logger.withMetadata({ workflowId: 'wf_1' })
|
||||
expect(child).toBeInstanceOf(Logger)
|
||||
expect(child).not.toBe(logger)
|
||||
})
|
||||
|
||||
test('should include metadata in log output', () => {
|
||||
const child = createEnabledLogger().withMetadata({ workflowId: 'wf_1' })
|
||||
child.info('hello')
|
||||
expect(consoleLogSpy).toHaveBeenCalledTimes(1)
|
||||
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
|
||||
expect(parsed.workflowId).toBe('wf_1')
|
||||
expect(parsed.message).toBe('hello')
|
||||
})
|
||||
|
||||
test('should not affect original logger output', () => {
|
||||
const logger = createEnabledLogger()
|
||||
logger.withMetadata({ workflowId: 'wf_1' })
|
||||
logger.info('hello')
|
||||
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
|
||||
expect(parsed.workflowId).toBeUndefined()
|
||||
})
|
||||
|
||||
test('should merge metadata across chained calls', () => {
|
||||
const child = createEnabledLogger().withMetadata({ a: '1' }).withMetadata({ b: '2' })
|
||||
child.info('hello')
|
||||
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
|
||||
expect(parsed.a).toBe('1')
|
||||
expect(parsed.b).toBe('2')
|
||||
})
|
||||
|
||||
test('should override parent metadata for same key', () => {
|
||||
const child = createEnabledLogger().withMetadata({ a: '1' }).withMetadata({ a: '2' })
|
||||
child.info('hello')
|
||||
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
|
||||
expect(parsed.a).toBe('2')
|
||||
})
|
||||
|
||||
test('should exclude undefined values from output', () => {
|
||||
const child = createEnabledLogger().withMetadata({ a: '1', b: undefined })
|
||||
child.info('hello')
|
||||
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
|
||||
expect(parsed.a).toBe('1')
|
||||
expect(parsed.b).toBeUndefined()
|
||||
})
|
||||
|
||||
test('should produce no metadata segment when metadata is empty', () => {
|
||||
const child = createEnabledLogger().withMetadata({})
|
||||
child.info('hello')
|
||||
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
|
||||
expect(parsed.message).toBe('hello')
|
||||
expect(Object.keys(parsed)).toEqual(
|
||||
expect.arrayContaining(['timestamp', 'level', 'module', 'message'])
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* @sim/logger
|
||||
*
|
||||
* Framework-agnostic logging utilities for the Sim platform.
|
||||
* Provides standardized console logging with environment-aware configuration.
|
||||
*/
|
||||
import { filterUndefined } from '@sim/utils/object'
|
||||
import chalk from 'chalk'
|
||||
import { getRequestContext } from './request-context'
|
||||
|
||||
/**
|
||||
* LogLevel enum defines the severity levels for logging
|
||||
*
|
||||
* DEBUG: Detailed information, typically useful only for diagnosing problems
|
||||
* INFO: Confirmation that things are working as expected
|
||||
* WARN: Indication that something unexpected happened
|
||||
* ERROR: Error events that might still allow the application to continue running
|
||||
*/
|
||||
export enum LogLevel {
|
||||
DEBUG = 'DEBUG',
|
||||
INFO = 'INFO',
|
||||
WARN = 'WARN',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger configuration options
|
||||
*/
|
||||
export interface LoggerConfig {
|
||||
/** Minimum log level to display */
|
||||
logLevel?: LogLevel | string
|
||||
/** Whether to colorize output */
|
||||
colorize?: boolean
|
||||
/** Whether logging is enabled */
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata key-value pairs attached to a logger instance.
|
||||
* Included automatically in every log line produced by that logger.
|
||||
*/
|
||||
export type LoggerMetadata = Record<string, string | number | boolean | undefined>
|
||||
|
||||
const getNodeEnv = (): string => {
|
||||
if (typeof process !== 'undefined' && process.env) {
|
||||
return process.env.NODE_ENV || 'development'
|
||||
}
|
||||
return 'development'
|
||||
}
|
||||
|
||||
const getLogLevel = (): string | undefined => {
|
||||
if (typeof process !== 'undefined' && process.env) {
|
||||
return process.env.LOG_LEVEL
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimum log level from environment variable or use defaults
|
||||
* - Development: DEBUG (show all logs)
|
||||
* - Production: ERROR (only show errors, but can be overridden by LOG_LEVEL env var)
|
||||
* - Test: ERROR (only show errors in tests)
|
||||
*/
|
||||
const getMinLogLevel = (): LogLevel => {
|
||||
const logLevelEnv = getLogLevel()
|
||||
if (logLevelEnv && Object.values(LogLevel).includes(logLevelEnv as LogLevel)) {
|
||||
return logLevelEnv as LogLevel
|
||||
}
|
||||
|
||||
const nodeEnv = getNodeEnv()
|
||||
switch (nodeEnv) {
|
||||
case 'development':
|
||||
return LogLevel.DEBUG
|
||||
case 'production':
|
||||
return LogLevel.ERROR
|
||||
case 'test':
|
||||
return LogLevel.ERROR
|
||||
default:
|
||||
return LogLevel.DEBUG
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for different environments
|
||||
*/
|
||||
const getLogConfig = () => {
|
||||
const nodeEnv = getNodeEnv()
|
||||
const minLevel = getMinLogLevel()
|
||||
|
||||
switch (nodeEnv) {
|
||||
case 'development':
|
||||
return {
|
||||
enabled: true,
|
||||
minLevel,
|
||||
colorize: true,
|
||||
}
|
||||
case 'production':
|
||||
return {
|
||||
enabled: true,
|
||||
minLevel,
|
||||
colorize: false,
|
||||
}
|
||||
case 'test':
|
||||
return {
|
||||
enabled: false,
|
||||
minLevel,
|
||||
colorize: false,
|
||||
}
|
||||
default:
|
||||
return {
|
||||
enabled: true,
|
||||
minLevel,
|
||||
colorize: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format objects for logging
|
||||
*/
|
||||
const formatObject = (obj: unknown, isDev: boolean): string => {
|
||||
try {
|
||||
if (obj instanceof Error) {
|
||||
const errorObj: Record<string, unknown> = {
|
||||
message: obj.message,
|
||||
stack: isDev ? obj.stack : undefined,
|
||||
name: obj.name,
|
||||
}
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (!(key in errorObj)) {
|
||||
errorObj[key] = (obj as unknown as Record<string, unknown>)[key]
|
||||
}
|
||||
}
|
||||
return JSON.stringify(errorObj, null, isDev ? 2 : 0)
|
||||
}
|
||||
return JSON.stringify(obj, null, isDev ? 2 : 0)
|
||||
} catch {
|
||||
return '[Circular or Non-Serializable Object]'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger class for standardized console logging
|
||||
*
|
||||
* Provides methods for logging at different severity levels
|
||||
* and handles formatting, colorization, and environment-specific behavior.
|
||||
*/
|
||||
export class Logger {
|
||||
private module: string
|
||||
private config: ReturnType<typeof getLogConfig>
|
||||
private isDev: boolean
|
||||
private metadata: LoggerMetadata = {}
|
||||
|
||||
/**
|
||||
* Create a new logger for a specific module
|
||||
* @param module The name of the module (e.g., 'OpenAIProvider', 'AgentBlockHandler')
|
||||
* @param overrideConfig Optional configuration overrides
|
||||
*/
|
||||
constructor(module: string, overrideConfig?: LoggerConfig) {
|
||||
this.module = module
|
||||
this.config = getLogConfig()
|
||||
this.isDev = getNodeEnv() === 'development'
|
||||
|
||||
// Apply overrides if provided
|
||||
if (overrideConfig) {
|
||||
if (overrideConfig.logLevel !== undefined) {
|
||||
const level =
|
||||
typeof overrideConfig.logLevel === 'string'
|
||||
? (overrideConfig.logLevel as LogLevel)
|
||||
: overrideConfig.logLevel
|
||||
if (Object.values(LogLevel).includes(level)) {
|
||||
this.config.minLevel = level
|
||||
}
|
||||
}
|
||||
if (overrideConfig.colorize !== undefined) {
|
||||
this.config.colorize = overrideConfig.colorize
|
||||
}
|
||||
if (overrideConfig.enabled !== undefined) {
|
||||
this.config.enabled = overrideConfig.enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a child logger with additional metadata merged in.
|
||||
* The child inherits this logger's module name, config, and existing metadata.
|
||||
* New metadata keys override existing ones with the same name.
|
||||
*/
|
||||
withMetadata(metadata: LoggerMetadata): Logger {
|
||||
const child = Object.create(Logger.prototype) as Logger
|
||||
child.module = this.module
|
||||
child.config = this.config
|
||||
child.isDev = this.isDev
|
||||
child.metadata = { ...this.metadata, ...metadata }
|
||||
return child
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a log at the given level should be displayed
|
||||
*/
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
if (!this.config.enabled) return false
|
||||
|
||||
if (
|
||||
getNodeEnv() === 'production' &&
|
||||
typeof (globalThis as { window?: unknown }).window !== 'undefined'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR]
|
||||
const minLevelIndex = levels.indexOf(this.config.minLevel)
|
||||
const currentLevelIndex = levels.indexOf(level)
|
||||
|
||||
return currentLevelIndex >= minLevelIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* Format arguments for logging, converting objects to JSON strings
|
||||
*/
|
||||
private formatArgs(args: unknown[]): unknown[] {
|
||||
return args.map((arg) => {
|
||||
if (arg === null || arg === undefined) return arg
|
||||
if (typeof arg === 'object') return formatObject(arg, this.isDev)
|
||||
return arg
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to log a message with the specified level
|
||||
*/
|
||||
private log(level: LogLevel, message: string, ...args: unknown[]) {
|
||||
if (!this.shouldLog(level)) return
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
const formattedArgs = this.formatArgs(args)
|
||||
|
||||
const reqCtx = getRequestContext()
|
||||
const effectiveMetadata = reqCtx
|
||||
? {
|
||||
requestId: reqCtx.requestId,
|
||||
method: reqCtx.method,
|
||||
path: reqCtx.path,
|
||||
...this.metadata,
|
||||
}
|
||||
: this.metadata
|
||||
const metadataEntries = Object.entries(filterUndefined(effectiveMetadata))
|
||||
const metadataStr =
|
||||
metadataEntries.length > 0
|
||||
? ` {${metadataEntries.map(([k, v]) => `${k}=${v}`).join(' ')}}`
|
||||
: ''
|
||||
|
||||
if (this.config.colorize) {
|
||||
let levelColor: (text: string) => string
|
||||
const moduleColor = chalk.cyan
|
||||
const timestampColor = chalk.gray
|
||||
|
||||
switch (level) {
|
||||
case LogLevel.DEBUG:
|
||||
levelColor = chalk.blue
|
||||
break
|
||||
case LogLevel.INFO:
|
||||
levelColor = chalk.green
|
||||
break
|
||||
case LogLevel.WARN:
|
||||
levelColor = chalk.yellow
|
||||
break
|
||||
case LogLevel.ERROR:
|
||||
levelColor = chalk.red
|
||||
break
|
||||
}
|
||||
|
||||
const coloredMeta = metadataStr ? ` ${chalk.magenta(metadataStr.trim())}` : ''
|
||||
const coloredPrefix = `${timestampColor(`[${timestamp}]`)} ${levelColor(`[${level}]`)} ${moduleColor(`[${this.module}]`)}${coloredMeta}`
|
||||
|
||||
if (level === LogLevel.ERROR) {
|
||||
console.error(coloredPrefix, message, ...formattedArgs)
|
||||
} else {
|
||||
console.log(coloredPrefix, message, ...formattedArgs)
|
||||
}
|
||||
} else {
|
||||
// Structured JSON for production — CloudWatch Log Insights auto-parses JSON lines
|
||||
const entry: Record<string, unknown> = {
|
||||
timestamp,
|
||||
level,
|
||||
module: this.module,
|
||||
message,
|
||||
}
|
||||
for (const [k, v] of metadataEntries) {
|
||||
entry[k] = v
|
||||
}
|
||||
// Merge extra args into the entry
|
||||
for (const arg of args) {
|
||||
if (
|
||||
arg !== null &&
|
||||
arg !== undefined &&
|
||||
typeof arg === 'object' &&
|
||||
!(arg instanceof Error)
|
||||
) {
|
||||
Object.assign(entry, arg)
|
||||
} else if (arg instanceof Error) {
|
||||
entry.error = arg.message
|
||||
entry.stack = arg.stack
|
||||
} else if (arg !== null && arg !== undefined) {
|
||||
entry.extra = arg
|
||||
}
|
||||
}
|
||||
|
||||
const line = JSON.stringify(entry)
|
||||
if (level === LogLevel.ERROR) {
|
||||
console.error(line)
|
||||
} else {
|
||||
console.log(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a debug message
|
||||
*
|
||||
* Use for detailed information useful during development and debugging.
|
||||
* These logs are only shown in development environment by default.
|
||||
*/
|
||||
debug(message: string, ...args: unknown[]) {
|
||||
this.log(LogLevel.DEBUG, message, ...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an info message
|
||||
*
|
||||
* Use for general information about application operation.
|
||||
*/
|
||||
info(message: string, ...args: unknown[]) {
|
||||
this.log(LogLevel.INFO, message, ...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a warning message
|
||||
*
|
||||
* Use for potentially problematic situations that don't cause operation failure.
|
||||
*/
|
||||
warn(message: string, ...args: unknown[]) {
|
||||
this.log(LogLevel.WARN, message, ...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message
|
||||
*
|
||||
* Use for error events that might still allow the application to continue.
|
||||
*/
|
||||
error(message: string, ...args: unknown[]) {
|
||||
this.log(LogLevel.ERROR, message, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a logger for a specific module
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createLogger } from '@sim/logger'
|
||||
*
|
||||
* const logger = createLogger('MyComponent')
|
||||
*
|
||||
* logger.debug('Initializing component', { props })
|
||||
* logger.info('Component mounted')
|
||||
* logger.warn('Deprecated prop used', { propName })
|
||||
* logger.error('Failed to fetch data', error)
|
||||
* ```
|
||||
*
|
||||
* @param module The name of the module
|
||||
* @param config Optional configuration overrides
|
||||
* @returns A Logger instance
|
||||
*/
|
||||
export function createLogger(module: string, config?: LoggerConfig): Logger {
|
||||
return new Logger(module, config)
|
||||
}
|
||||
|
||||
export type { RequestContext } from './request-context'
|
||||
export { getRequestContext, runWithRequestContext } from './request-context'
|
||||
@@ -0,0 +1,46 @@
|
||||
export interface RequestContext {
|
||||
requestId: string
|
||||
method?: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* AsyncLocalStorage is only available in Node.js. In Edge/browser contexts
|
||||
* we fall back to a no-op implementation so the logger import doesn't break.
|
||||
*/
|
||||
interface Storage<T> {
|
||||
getStore(): T | undefined
|
||||
run<R>(store: T, fn: () => R): R
|
||||
}
|
||||
|
||||
let storage: Storage<RequestContext>
|
||||
|
||||
if (typeof globalThis.process !== 'undefined' && globalThis.process.versions?.node) {
|
||||
// Node.js — use real AsyncLocalStorage
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { AsyncLocalStorage } = require('node:async_hooks') as typeof import('node:async_hooks')
|
||||
storage = new AsyncLocalStorage<RequestContext>()
|
||||
} else {
|
||||
// Edge / browser — no-op
|
||||
storage = {
|
||||
getStore: () => undefined,
|
||||
run: <R>(_store: RequestContext, fn: () => R) => fn(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a callback within a request context. All loggers called inside
|
||||
* the callback (and any async functions it awaits) will automatically
|
||||
* include the request context metadata in their output.
|
||||
*/
|
||||
export function runWithRequestContext<T>(context: RequestContext, fn: () => T): T {
|
||||
return storage.run(context, fn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current request context, or undefined if called outside
|
||||
* of a `runWithRequestContext` scope.
|
||||
*/
|
||||
export function getRequestContext(): RequestContext | undefined {
|
||||
return storage.getStore()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "@sim/tsconfig/library.json",
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user