From d088161d00d4ce18a274f7fb7b6f07908ac2116b Mon Sep 17 00:00:00 2001 From: Colten Krauter <18080897+coltenkrauter@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:30:14 -0700 Subject: [PATCH] feat: support hiding fields --- README.md | 11 +++- package-lock.json | 4 +- package.json | 2 +- src/config.ts | 9 ++- src/logger.ts | 24 ++++++-- src/structures.ts | 5 ++ test/logger.test.ts | 140 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 185 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1ae2dc4..51c86d1 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ A TypeScript logging utility optimized for structured logging in AWS Lambda envi - **Singleton Pattern**: Ensures consistent configuration across the application. - **Environment-Based Configurations**: Configure log formats, levels, and transports using environment variables. - **Persistent Metadata**: Easily add or remove metadata fields from all subsequent logs via `addToAllLogs` and `removeFromAllLogs`. +- **Hidden Fields Management**: Control which metadata fields are excluded from logs via `LOG_FRIENDLY_FIELDS_HIDE` and `LOG_STRUCTURED_FIELDS_HIDE`. ## Installation @@ -69,6 +70,11 @@ logger.error('an error occurred', { errorDetails: 'error details here' }) // remove metadata keys (single or multiple) logger.removeFromAllLogs('userId', 'sessionId') +// dynamically hide specific fields from logs +logger.updateInstance({ + configOptions: { LOG_FRIENDLY_FIELDS_HIDE: ['sessionId'] }, +}) + // publish metrics to cloudwatch await logger.publishMetric({ metricName: 'requestCount', @@ -82,9 +88,12 @@ await logger.publishMetric({ The logger supports multiple configuration options to control logging format, levels, and transports. Some commonly used environment variables include: - `LOG_LEVEL`: Set the log level (`debug`, `info`, `warn`, `error`). -- `LOG_FORMAT`: Choose between `structured` for json logging and `friendly` for colorized console output. (default: friendly) +- `LOG_FORMAT`: Choose between `structured` for JSON logging and `friendly` for colorized console output (default: friendly). +- `LOG_FRIENDLY_FIELDS_HIDE`: A comma-separated list of metadata fields to exclude from friendly logs. +- `LOG_STRUCTURED_FIELDS_HIDE`: A comma-separated list of metadata fields to exclude from structured logs. - `REQUEST_ID`: Optionally set a request ID for tracing log entries. - `SIMPLE_LOGS`: Optionally make log entries simpler (omit codename, version, use shorter requestId–useful for local development). +- `INIT_LOGGER`: Automatically initialize the logger if not set to `false`. ### DotEnv diff --git a/package-lock.json b/package-lock.json index 4bdfcf5..5c38da5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@krauters/logger", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@krauters/logger", - "version": "1.1.0", + "version": "1.2.0", "license": "ISC", "dependencies": { "@aws-sdk/client-cloudwatch": "^3.705.0", diff --git a/package.json b/package.json index 2cac74e..7237501 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@krauters/logger", "description": "TypeScript wrapper for Winston, optimized for structured Lambda logs and CloudWatch.", - "version": "1.1.0", + "version": "1.2.0", "main": "dist/src/index.js", "type": "commonjs", "scripts": { diff --git a/src/config.ts b/src/config.ts index bc4bcfc..e52cc48 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,8 +13,10 @@ export interface ConfigOptions { ENV: Env HOST: string LOG_FORMAT: string + LOG_FRIENDLY_FIELDS_HIDE?: string[] LOG_LEVEL: LogLevel LOG_SECTION_SEPARATOR: string + LOG_STRUCTURED_FIELDS_HIDE?: string[] PACKAGE: string STAGE: Stage TIMESTAMP_FORMAT: string @@ -41,8 +43,13 @@ export function getConfig(options?: Partial) { 'TIMESTAMP_FORMAT', 'VERSION', ) - .optionals('REQUEST_ID') + .optionals('REQUEST_ID', 'LOG_FRIENDLY_FIELDS_HIDE', 'LOG_STRUCTURED_FIELDS_HIDE') .transform((value) => !isFalsy(value), 'SIMPLE_LOGS') + .transform( + (value) => (value.trim() === '' ? [] : value.replace(/\s/g, '').split(',')), + 'LOG_FRIENDLY_FIELDS_HIDE', + 'LOG_STRUCTURED_FIELDS_HIDE', + ) .transform((value) => value as Env, 'ENV') .transform((value) => value as Stage, 'STAGE') .transform((value) => value as LogLevel, 'LOG_LEVEL') diff --git a/src/logger.ts b/src/logger.ts index 7078e31..22167f0 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -11,7 +11,7 @@ import { v4 as uuidv4 } from 'uuid' import { addColors, createLogger, format, transports, config as winstonConfig } from 'winston' import type { Config } from './config' -import type { LoggerOptions, LogOptions, PublishMetricOptions } from './structures' +import type { GetLogObjectParams, LoggerOptions, LogOptions, PublishMetricOptions } from './structures' import { getConfig } from './config' import { empty, LogLevel } from './structures' @@ -104,12 +104,24 @@ export class Logger { return format.combine( format.colorize({ all: true }), format.timestamp({ format: this.config.TIMESTAMP_FORMAT }), - format.printf((info) => this.formatLogMessage(info, separator)), + format.printf((info) => + this.formatLogMessage( + this.getLogObject({ fieldsToHide: this.config.LOG_FRIENDLY_FIELDS_HIDE, info }), + separator, + ), + ), ) } - public getLogObject(info: Record): Record { - return { ...this.metadata, ...info } + public getLogObject({ fieldsToHide = [], info }: GetLogObjectParams): Record { + const combined = { ...this.metadata, ...info } + + for (const field of fieldsToHide) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete combined[field] + } + + return combined } public getRequestId(context?: LambdaContext): string { @@ -123,7 +135,9 @@ export class Logger { public getStructuredFormat(): Format { return format.combine( format.timestamp({ format: this.config.TIMESTAMP_FORMAT }), - format.printf((info) => JSON.stringify(this.getLogObject(info))), + format.printf((info) => + JSON.stringify(this.getLogObject({ fieldsToHide: this.config.LOG_STRUCTURED_FIELDS_HIDE, info })), + ), ) } diff --git a/src/structures.ts b/src/structures.ts index efbab5c..32d5cbe 100644 --- a/src/structures.ts +++ b/src/structures.ts @@ -45,3 +45,8 @@ export interface PublishMetricOptions { } export const empty = 'NOTSET' + +export interface GetLogObjectParams { + fieldsToHide?: string[] + info: Record +} diff --git a/test/logger.test.ts b/test/logger.test.ts index 3833000..d65d02b 100644 --- a/test/logger.test.ts +++ b/test/logger.test.ts @@ -9,6 +9,8 @@ import { PutMetricDataCommand } from '@aws-sdk/client-cloudwatch' import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals' import { Env } from '@krauters/structures' +import type { GetLogObjectParams } from '../src/index' + import { initializeLogger, Logger, LogLevel, MetricUnit } from '../src/index' jest.mock('@aws-sdk/client-cloudwatch') @@ -346,3 +348,141 @@ describe('Logger', () => { }).toThrow() }) }) + +describe('Logger.getLogObject', () => { + let logger: Logger + + beforeEach(() => { + logger = new Logger({ + configOptions: { + CODENAME: 'TEST', + ENV: Env.Development, + HOST: 'mock-host', + LOG_FORMAT: 'friendly', + LOG_LEVEL: LogLevel.Debug, + LOG_SECTION_SEPARATOR: ' | ', + PACKAGE: 'logger-package', + STAGE: Env.Beta, + TIMESTAMP_FORMAT: 'YYYY-MM-DD HH:mm:ssZ', + VERSION: '1.0.0', + }, + }) + + // Ensure consistent metadata across all tests + logger.metadata = { + codename: 'TEST', + host: 'mock-host', + package: 'logger-package', + stage: 'Beta', + version: '1.0.0', + } + }) + + it('should combine metadata and info while excluding specified fields', () => { + logger.addToAllLogs('requestId', '1234') + + const originalMetadata = JSON.parse(JSON.stringify(logger.metadata)) + const originalInfo = { level: 'info', message: 'Test message', userId: 'user-5678' } + + const params: GetLogObjectParams = { + fieldsToHide: ['host', 'requestId'], + info: originalInfo, + } + + const result = logger.getLogObject(params) + + expect(result).toEqual({ + codename: 'TEST', + level: 'info', + message: 'Test message', + package: 'logger-package', + stage: 'Beta', + userId: 'user-5678', + version: '1.0.0', + }) + + // Validate original metadata and info are unchanged + expect(logger.metadata).toEqual(originalMetadata) + expect(originalInfo).toEqual({ level: 'info', message: 'Test message', userId: 'user-5678' }) + }) + + it('should handle empty fieldsToHide gracefully', () => { + logger.updateInstance({ configOptions: { LOG_FRIENDLY_FIELDS_HIDE: [] } }) + + const originalMetadata = JSON.parse(JSON.stringify(logger.metadata)) + const originalInfo = { level: 'info', message: 'Test message' } + + const params: GetLogObjectParams = { info: originalInfo } + + const result = logger.getLogObject(params) + + expect(result).toEqual({ ...originalMetadata, ...originalInfo }) + expect(logger.metadata).toEqual(originalMetadata) + }) + + it('should exclude fields defined in LOG_FRIENDLY_FIELDS_HIDE', () => { + const originalMetadata = JSON.parse(JSON.stringify(logger.metadata)) + const originalInfo = { level: 'info', message: 'Test message', userId: 'user-5678' } + + const params: GetLogObjectParams = { fieldsToHide: ['host', 'level'], info: originalInfo } + + const result = logger.getLogObject(params) + + expect(result).toEqual({ + codename: 'TEST', + message: 'Test message', + package: 'logger-package', + stage: 'Beta', + userId: 'user-5678', + version: '1.0.0', + }) + + // Validate original metadata and info are unchanged + expect(logger.metadata).toEqual(originalMetadata) + expect(originalInfo).toEqual({ level: 'info', message: 'Test message', userId: 'user-5678' }) + }) + + it('should return only metadata when info is empty', () => { + logger.updateInstance({ configOptions: { LOG_FRIENDLY_FIELDS_HIDE: ['stage'] } }) + + const originalMetadata = JSON.parse(JSON.stringify(logger.metadata)) + + const params: GetLogObjectParams = { info: {} } + + const result = logger.getLogObject(params) + + expect(result).toEqual(originalMetadata) + expect(logger.metadata).toEqual(originalMetadata) + }) + + it('should return an empty object when all fields are excluded', () => { + const originalMetadata = JSON.parse(JSON.stringify(logger.metadata)) + const originalInfo = { level: 'info', message: 'Test message' } + + const params: GetLogObjectParams = { + fieldsToHide: ['codename', 'host', 'package', 'stage', 'version', 'requestId', 'level', 'message'], + info: originalInfo, + } + + const result = logger.getLogObject(params) + + expect(result).toEqual({}) + expect(logger.metadata).toEqual(originalMetadata) + expect(originalInfo).toEqual({ level: 'info', message: 'Test message' }) + }) + + it('should handle undefined LOG_FRIENDLY_FIELDS_HIDE gracefully', () => { + const originalMetadata = JSON.parse(JSON.stringify(logger.metadata)) + const originalInfo = { level: 'info', message: 'Test message' } + + const params: GetLogObjectParams = { info: originalInfo } + + const result = logger.getLogObject(params) + + expect(result).toEqual({ ...originalMetadata, ...originalInfo }) + + // Validate original metadata and info are unchanged + expect(logger.metadata).toEqual(originalMetadata) + expect(originalInfo).toEqual({ level: 'info', message: 'Test message' }) + }) +})