Skip to content

Commit

Permalink
Merge pull request #13 from krauters/feature/coltenkrauter/2024-12-10/2
Browse files Browse the repository at this point in the history
Support Hiding Fields
  • Loading branch information
coltenkrauter authored Dec 10, 2024
2 parents 2963f60 + d088161 commit ba3e4bb
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 10 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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',
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
9 changes: 8 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,8 +43,13 @@ export function getConfig(options?: Partial<ConfigOptions>) {
'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')
Expand Down
24 changes: 19 additions & 5 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<string, unknown>): Record<string, unknown> {
return { ...this.metadata, ...info }
public getLogObject({ fieldsToHide = [], info }: GetLogObjectParams): Record<string, unknown> {
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 {
Expand All @@ -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 })),
),
)
}

Expand Down
5 changes: 5 additions & 0 deletions src/structures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,8 @@ export interface PublishMetricOptions {
}

export const empty = 'NOTSET'

export interface GetLogObjectParams {
fieldsToHide?: string[]
info: Record<string, unknown>
}
140 changes: 140 additions & 0 deletions test/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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' })
})
})

0 comments on commit ba3e4bb

Please sign in to comment.