Skip to content

Commit

Permalink
pluggable sentry logger
Browse files Browse the repository at this point in the history
  • Loading branch information
joshbalfour committed Apr 9, 2024
1 parent 2e44b51 commit 458c67b
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 58 deletions.
12 changes: 7 additions & 5 deletions packages/modules/edge-api-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "@reapit-cdk/edge-api-sdk",
"main": "src/index.ts",
"main": "dist/index.js",
"description": "Provides convenience wrappers for accepting and responding to [@reapit-cdk/edge-api]('../../constructs/edge-api/readme.md') lambda requests.",
"version": "0.0.0",
"version": "0.0.9",
"homepage": "https://github.com/reapit/ts-cdk-constructs/blob/main/packages/modules/edge-api-sdk",
"readme": "https://github.com/reapit/ts-cdk-constructs/blob/main/packages/modules/edge-api-sdk/readme.md",
"bugs": {
Expand All @@ -13,9 +13,6 @@
"url": "https://github.com/reapit/ts-cdk-constructs.git"
},
"license": "MIT",
"publishConfig": {
"main": "dist/index.js"
},
"scripts": {
"publish": "yarn npm publish",
"check": "yarn run root:check -p $(pwd)",
Expand All @@ -29,5 +26,10 @@
"@reapit-cdk/tsup": "workspace:^",
"@types/aws-lambda": "^8.10.121",
"aws-lambda": "^1.0.7"
},
"dependencies": {
"@sentry/node": "^7.109.0",
"@sentry/types": "^7.109.0",
"@sentry/utils": "^7.109.0"
}
}
22 changes: 17 additions & 5 deletions packages/modules/edge-api-sdk/src/edge-api-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as cloudfront from './cloudfront'
import * as httpApi from './http-api'

import { EventInput, JSONRequestHandler, RCHeaders, RCRequest, RCResponse, RequestHandler } from './types'
import { createLogger, panic } from './logger'
import { Logger, LoggerConfig, panic } from './logger'
import { sessionIdHeaderName } from './config'

const eventToRequest = <EnvType>(event: EventInput, context: Context): RCRequest<EnvType> => {
Expand Down Expand Up @@ -57,19 +57,25 @@ const respondToEvent = (event: EventInput, response: RCResponse) => {
}
}

export const requestHandler = <EnvType>(requestHandler: RequestHandler<EnvType>) => {
export type HandlerConfig = {
loggerConfig: LoggerConfig
}

export const requestHandler = <EnvType>(requestHandler: RequestHandler<EnvType>, handlerConfig: HandlerConfig) => {
const fn = async (event: EventInput, conext: Context) => {
try {
const request = eventToRequest<EnvType>(event, conext)
const logger = createLogger(request)
const logger = new Logger(request, handlerConfig?.loggerConfig)
if (request.method === 'OPTIONS') {
await logger.flush()
return respondToEvent(event, {
status: 200,
headers: {},
})
}
try {
const response = await requestHandler({ ...request, logger })
await logger.flush()
return respondToEvent(event, response)
} catch (e) {
logger.error(e)
Expand Down Expand Up @@ -103,13 +109,15 @@ const errorResponseToEvent = (event: EventInput, err: Error) => {

export const jsonRequestHandler = <EnvType, BodyType = any>(
jsonRequestHandler: JSONRequestHandler<EnvType, BodyType>,
handlerConfig?: HandlerConfig,
) => {
const fn = async (event: EventInput, context: Context) => {
try {
const request = eventToRequest<EnvType>(event, context)
const logger = createLogger(request)
const logger = new Logger(request, handlerConfig?.loggerConfig)
const body = request.body ? JSON.parse(request.body) : undefined
if (request.method === 'OPTIONS') {
await logger.flush()
return respondToEvent(event, {
status: 200,
headers: {},
Expand All @@ -122,6 +130,7 @@ export const jsonRequestHandler = <EnvType, BodyType = any>(
body: body as BodyType | undefined,
logger,
})
await logger.flush()

if (response.status === 302) {
return respondToEvent(event, {
Expand Down Expand Up @@ -157,13 +166,15 @@ export const jsonRequestHandler = <EnvType, BodyType = any>(

export const formRequestHandler = <EnvType, BodyType = any>(
formRequestHandler: JSONRequestHandler<EnvType, BodyType>,
handlerConfig?: HandlerConfig,
) => {
const fn = async (event: EventInput, context: Context) => {
try {
const request = eventToRequest<EnvType>(event, context)
const logger = createLogger(request)
const logger = new Logger(request, handlerConfig?.loggerConfig)
const body = request.body ? Object.fromEntries(new URLSearchParams(request.body)) : undefined
if (request.method === 'OPTIONS') {
await logger.flush()
return respondToEvent(event, {
status: 200,
headers: {},
Expand All @@ -176,6 +187,7 @@ export const formRequestHandler = <EnvType, BodyType = any>(
body: body as BodyType | undefined,
logger,
})
await logger.flush()

if (response.status === 302) {
return respondToEvent(event, {
Expand Down
1 change: 1 addition & 0 deletions packages/modules/edge-api-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './edge-api-sdk'
export * from './types'
export * from './config'
export * from './sentry'
113 changes: 68 additions & 45 deletions packages/modules/edge-api-sdk/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { EventInput, JSONRequest, RCRequest } from './types'
import { format } from 'util'

type LogFn = (message?: any, ...optionalParams: any[]) => void
export type LogLevel = 'info' | 'warning' | 'error' | 'critical' | 'panic'

type LogLevel = 'info' | 'warning' | 'error' | 'critical' | 'panic'

export type Logger = Record<LogLevel, LogFn>

type LogEntry = {
export type LogEntry = {
level: LogLevel
message: string
timestamp: Date
error?: Error
}

type MinimalLogPayload = {
export type MinimalLogPayload = {
event: EventInput
functionName: string
functionVersion: string
Expand Down Expand Up @@ -73,30 +69,10 @@ const scrub = <T extends MinimalLogPayload | LogPayload>(payload: T): T => {
}
}

const writeOut = (payload: MinimalLogPayload | LogPayload) => {
console.log(JSON.stringify(scrub(payload)))
}

const flush = (request: RCRequest<any>, entries: LogEntry[], centralize: boolean) => {
const {
meta: { functionName, functionVersion, invocationId, sessionId, event },
region,
} = request

const payload: LogPayload = {
entries,
event,
functionName,
functionVersion,
invocationId,
sessionId,
region,
request,
centralize,
timestamp: new Date(),
}
export type Transport = (payload: MinimalLogPayload | LogPayload) => void | Promise<void>

writeOut(payload)
export const consoleTransport = (payload: MinimalLogPayload | LogPayload) => {
console.log(JSON.stringify(scrub(payload)))
}

type AdditionalLogData = {
Expand Down Expand Up @@ -128,7 +104,7 @@ export const panic = (error: Error, event: EventInput) => {
},
],
}
writeOut(payload)
consoleTransport(payload)
}

const stringifiableError = (error?: Error): Error | undefined => {
Expand All @@ -142,12 +118,18 @@ const stringifiableError = (error?: Error): Error | undefined => {
}
}

export const createLogger = (request: RCRequest<any>): Logger => {
const entries: LogEntry[] = []
export type LoggerConfig = {
transports: Transport[]
}

const logFn =
(level: LogLevel): LogFn =>
(...args) => {
export class Logger {
private request: RCRequest<any>
private entries: LogEntry[] = []
private transports: Transport[]
private queue: (void | Promise<void>)[] = []

private logFn(level: LogLevel) {
return (...args: any[]) => {
if (level === 'warning') {
console.warn(...args)
} else if (level === 'critical' || level === 'error' || level === 'panic') {
Expand All @@ -157,23 +139,64 @@ export const createLogger = (request: RCRequest<any>): Logger => {
} else {
console.log(...args)
}
entries.push({
this.entries.push({
level,
message: format(...args),
timestamp: new Date(),
error: stringifiableError(args.find(isAdditionalLogData)?.error),
})
const centralize = level === 'error' || level === 'critical'
const centralize = level === 'error' || level === 'critical' || level === 'panic'
if (centralize) {
flush(request, entries, centralize)
this.writeOut(centralize)
}
}
}

return {
info: logFn('info'),
warning: logFn('warning'),
error: logFn('error'),
critical: logFn('critical'),
panic: logFn('panic'),
private writeOut(centralize: boolean) {
const {
meta: { functionName, functionVersion, invocationId, sessionId, event },
region,
} = this.request

const payload: LogPayload = {
entries: this.entries,
event,
functionName,
functionVersion,
invocationId,
sessionId,
region,
request: this.request,
centralize,
timestamp: new Date(),
}

this.queue.push(...this.transports.map((transport) => transport(payload)))
}

info(...args: any[]) {
this.logFn('info')(...args)
}

warning(...args: any[]) {
this.logFn('warning')(...args)
}
error(...args: any[]) {
this.logFn('error')(...args)
}
critical(...args: any[]) {
this.logFn('critical')(...args)
}
panic(...args: any[]) {
this.logFn('panic')(...args)
}

async flush() {
await Promise.all(this.queue)
}

constructor(request: RCRequest<any>, config?: LoggerConfig) {
this.request = request
this.transports = config?.transports || [consoleTransport]
}
}
59 changes: 59 additions & 0 deletions packages/modules/edge-api-sdk/src/sentry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { NodeClient, createTransport, createGetModuleFromFilename } from '@sentry/node'
import { NodeTransportOptions } from '@sentry/node/types/transports'
import { Transport as SentryTransport, SeverityLevel } from '@sentry/types'
import { stackParserFromStackParserOptions, createStackParser, nodeStackLineParser } from '@sentry/utils'
import { LogLevel, LogPayload, MinimalLogPayload, Transport } from './logger'

const sentryTransport: (transportOptions: NodeTransportOptions) => SentryTransport = (options) => {
return createTransport(options, async (request) => {
const response = await fetch(options.url, {
body: request.body,
method: 'POST',
referrerPolicy: 'origin',
headers: options.headers,
})
return {
statusCode: response.status,
headers: {
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
'retry-after': response.headers.get('Retry-After'),
},
}
})
}

const stackParser = stackParserFromStackParserOptions(
createStackParser(nodeStackLineParser(createGetModuleFromFilename())),
)

export type InitProps = {
dsn: string
environment?: string
release?: string
}

const logLevelToSeverityLevel = (logLevel: LogLevel): SeverityLevel => {
if (logLevel === 'panic' || logLevel === 'critical') {
return 'fatal'
}
return logLevel
}

export const init = (props: InitProps): Transport => {
const client = new NodeClient({
integrations: [],
transport: sentryTransport,
stackParser,
...props,
})

return async (payload: MinimalLogPayload | LogPayload) => {
payload.entries.forEach((entry) => {
client.captureMessage(entry.message, logLevelToSeverityLevel(entry.level))
if (entry.error) {
client.captureException(entry.error)
}
})
await client.flush()
}
}
26 changes: 23 additions & 3 deletions packages/modules/edge-api-sdk/tests/logger.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CloudFrontRequestEvent } from 'aws-lambda'
import { JSONRequest } from '../src'
import { LogPayload, createLogger, panic } from '../src/logger'
import { LogPayload, Logger, LoggerConfig, panic } from '../src/logger'

const generateCloudfrontRequest = ({
headers = {
Expand Down Expand Up @@ -56,7 +56,7 @@ const generateCloudfrontRequest = ({
}
}

const getLogger = (body: any = { password: 'user-password' }) => {
const getLogger = (body: any = { password: 'user-password' }, config?: LoggerConfig) => {
const request: JSONRequest<any, any> = {
env: {},
headers: {
Expand All @@ -76,7 +76,7 @@ const getLogger = (body: any = { password: 'user-password' }) => {
sessionId: 'session-id',
},
}
const logger = createLogger(request)
const logger = new Logger(request, config)
return logger
}

Expand Down Expand Up @@ -207,4 +207,24 @@ describe('logger', () => {
expect(format.entries[0].error?.name).toBe('Big Error')
expect(format.entries[0].level).toBe('panic')
})

it('transports - should be called', () => {
const transportMock = jest.fn()
const logger = getLogger(undefined, {
transports: [transportMock],
})
logger.info('something')
logger.info('something again')
logger.critical('oh no')
expect(transportMock).toHaveBeenCalledTimes(1)
const entries = transportMock.mock.calls[0][0].entries
expect(entries[0].level).toEqual('info')
expect(entries[0].message).toEqual('something')

expect(entries[1].level).toEqual('info')
expect(entries[1].message).toEqual('something again')

expect(entries[2].level).toEqual('critical')
expect(entries[2].message).toEqual('oh no')
})
})
Loading

0 comments on commit 458c67b

Please sign in to comment.