Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/grumpy-bananas-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@gradientedge/logger': major
---

Provide an optional parameter to transformError such that custom error transformation can be achieved by the caller.
67 changes: 19 additions & 48 deletions packages/logger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,54 +33,6 @@ const myLogger = new Logger({
myLogger.error('Major problem!')
```

## Using with `@gradientedge/als`

The `@gradientedge/als` is a simple suite of functions that allow you to
create `AsyncLocalStorage` via the `create` function, and then retrieve
that data via the `retrieve` function.

As you'll see in the [`index.ts`](./src/index.ts) file, we use JavaScript's
`Proxy` class to check to see if we have async local storage data available
to us each time any of the `Logger` methods are called. If we do, then we
check for `logger` property on that data object. If that exists, and is an
instance of `Logger`, then we call the log method on that `Logger` instance
rather than on the singleton instance.

Here's an example:

```typescript
import { Logger, LoggerLevel } from '@gradientedge/logger'

const myLogger = new Logger({
level: LoggerLevel.WARN,
baseData: {
requestId: '1234'
}
})

const context = {
logger: myLogger
}

als.create(context, async () => {

log.debug('Test message', { name: 'Jimmy' })

// Prints the following:
//
// {
// "level": "debug",
// "message": "Test message",
// "base": {
// "requestId": "1234"
// },
// "data": {
// "name": "Jimmy"
// }
// }
})
```

## Setting the log level with an environment variable

You can define a `LOGGER_LEVEL` environment variable with one of the
Expand All @@ -93,3 +45,22 @@ following values:

If set, this will be used as the default log level for any new instances
of the `Logger` class.

### Transforming logger output

If you want to transform the output of the logger, you can pass a function to the `transform` option when creating a new logger instance.

example:
```typescript
import { Logger } from '@gradientedge/logger'

const myLogger = new Logger({
transform: (message, level, timestamp) => {
return {
message: message.toUpperCase(),
level,
timestamp,
}
},
})
```
27 changes: 27 additions & 0 deletions packages/logger/src/__tests__/error.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { transformError } from '../transform/error'

describe('transformError', () => {
it('should transform an error object', () => {
const error = new Error('Test error message')
const transformedError = transformError(error)

expect(transformedError).toHaveProperty('message', 'Test error message')
expect(transformedError).toHaveProperty('stack')
})

it('should transform a nested error object', () => {
const originalError = new Error('Original error message')
const someError: any = {
name: 'MyError',
message: 'Some error message',
}
someError.originalError = originalError
const transformedError = transformError(someError)

expect(transformedError).toHaveProperty('message', 'Some error message')
expect(transformedError).toHaveProperty('name', 'MyError')
expect(transformedError).toHaveProperty('originalError')
expect(transformedError.originalError).toHaveProperty('message', 'Original error message')
expect(transformedError.originalError).toHaveProperty('stack')
})
})
23 changes: 23 additions & 0 deletions packages/logger/src/__tests__/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ describe('Logger', () => {
expect(logger.levelName).toBe('debug')
expect(logger.levelNumber).toBe(10)
})

it('should set the transformer', () => {
const transformer = jest.fn()
const logger = new Logger({ transport, transformer })

expect(logger.transformer).toBe(transformer)
})
})

describe('error', () => {
Expand Down Expand Up @@ -247,5 +254,21 @@ describe('Logger', () => {
'{"level":"error","timestamp":"2023-09-28T00:00:00.000Z","data":123}',
)
})

it('should use the custom transformer to modify output', () => {
const mockTransformerFn = jest.fn(() => ({
level: 'error',
timestamp: '2023-09-28T00:00:00.000Z',
message: 'a transformed message',
}))
const logger = new Logger({ transport, transformer: mockTransformerFn })
const mockTransportFn = jest.fn()

logger.process(mockTransportFn, LoggerLevel.ERROR, ['a message'])

expect(mockTransportFn).toHaveBeenCalledWith(
'{"level":"error","timestamp":"2023-09-28T00:00:00.000Z","message":"a transformed message"}',
)
})
})
})
16 changes: 14 additions & 2 deletions packages/logger/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DEFAULT_LOG_LEVEL, LOG_BUFFER_ENABLED, VALID_LOGGER_LEVEL_VALUES, LogLevelNumber } from './constants'
import { LoggerOptions, LoggerTransport, LoggerLevelValue } from './types'
import { LoggerOptions, LoggerOutput, LoggerTransport, LoggerLevelValue } from './types'
import { generateOutput } from './output'
import stringify from 'json-stringify-safe'
import { Console } from 'node:console'
Expand All @@ -10,6 +10,9 @@ export interface LogBuffer {
timestamp: string
args: any[]
}
export interface Transformer {
(input: LoggerOutput): LoggerOutput
}

export class Logger {
public baseData: Record<string, any> | null
Expand All @@ -19,6 +22,7 @@ export class Logger {
public buffer: LogBuffer[]
public bufferEnabled: boolean
public bufferInitiallyEnabled: boolean
public transformer?: Transformer

constructor(options?: LoggerOptions) {
this.buffer = []
Expand Down Expand Up @@ -47,6 +51,10 @@ export class Logger {
}

this.bufferInitiallyEnabled = this.bufferEnabled

if (options?.transformer !== undefined) {
this.transformer = options.transformer
}
}

debug(...args: any[]) {
Expand Down Expand Up @@ -89,7 +97,11 @@ export class Logger {
timestamp = new Date().toISOString()
}

const output = generateOutput(level, timestamp, this.baseData, args)
let output: LoggerOutput = generateOutput(level, timestamp, this.baseData, args)

if (this.transformer) {
output = this.transformer(output)
}

method(stringify(output))
}
Expand Down
67 changes: 6 additions & 61 deletions packages/logger/src/transform/error.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,20 @@
/**
* Transform an Error object in to something more digestible.
* We do some specific transformation of Axios errors.
* Transform an error object in to something more digestible.
*
* @param error - The error object to transform.
* @param recursionLevel - The current recursion level (default: 0).
* @returns The transformed error object.
*/
export function transformError(error: Error & Record<string, any>, recursionLevel = 0): any {
if (recursionLevel > 5) {
return
}
let simpleError: Record<string, any> = {}
const simpleError: Record<string, any> = {}
Object.getOwnPropertyNames(error).forEach(function (key) {
simpleError[key] = error[key]
})
if (error.name === 'GraphQLError') {
return transformGraphQLError(error, recursionLevel + 1)
}
if (error?.isAxiosError) {
simpleError = pickAxiosErrorFields(simpleError)
}
if (error?.data?.error?.isAxiosError) {
simpleError.data.error = pickAxiosErrorFields(simpleError)
}
if (simpleError.originalError) {
simpleError.originalError = transformError(error.originalError, recursionLevel + 1)
}
return simpleError
}

/**
* Return only the fields we really want to see from the Axios error object.
* If we don't do this, then we get many thousands of lines of log lines as
* the HttpsAgent is a huge deeply nested object.
*/
export function pickAxiosErrorFields(error: any) {
let config: any
let response: any
if (error?.config) {
config = {
url: error?.config?.url,
method: error?.config?.method,
headers: error?.config?.headers,
timeout: error?.config?.timeout,
params: error?.config?.params,
}
}
if (error?.response) {
response = {
status: error?.response?.status,
headers: error?.response?.headers,
data: error?.response?.data,
}
}
return {
message: error?.message,
name: error?.name,
code: error?.code,
stack: error?.stack,
config,
response,
}
}

/**
* Return a lightweight GraphQL error object. In particular, we don't want the `nodes`
* property, which potentially contains a massive object containing most of the GraphQL
* schema object.
*/
export function transformGraphQLError(error: any, recursionLevel: number): any {
return {
message: error?.message,
stack: error?.stack,
locations: error?.locations,
path: error?.path,
originalError: error?.originalError && transformError(error.originalError, recursionLevel + 1),
}
}
1 change: 1 addition & 0 deletions packages/logger/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface LoggerOptions {
baseData?: Record<string, any>
transport?: LoggerTransport
bufferEnabled?: boolean
transformer?: any
}

/**
Expand Down
Loading