Skip to content

Commit

Permalink
feat: profiliing feature
Browse files Browse the repository at this point in the history
 - added profiler
 - added tests for profiler as well as handlers
 - changed trace to debug in logger
 - removed void response from handler, since it made handler
   responses harder to test / work with

resolves: #6
  • Loading branch information
refactorthis committed Apr 18, 2024
1 parent 99f0a80 commit 999376e
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 20 deletions.
2 changes: 1 addition & 1 deletion packages/api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const createApi = <
opts = merge({}, apiOpts, opts)

return pipeline<ApiResultV2<TResponse>, TEvent>(opts).handler((event, context) => {
// TODO hack - write proper type mappings for other use cases.
// TODO level-up to ApiGatewayProxyEventV2 always and remove TEvent
const ev = event as APIGatewayProxyEventV2

return opts.handler({
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/middleware/api-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ export default <TResponseStruct, TEvent>(opts?: FuncyApiOptions) => {
if (opts?.monitoring?.cloudWatchMetrics)
pipe.use(cloudWatchMetricsMiddleware(opts?.monitoring?.cloudWatchMetrics))

if (opts?.monitoring?.logLevel === 'trace')
pipe.use(inputOutputLoggerMiddleware({ logger: logger.trace }))
if (opts?.monitoring?.logLevel === 'debug')
pipe.use(inputOutputLoggerMiddleware({ logger: logger.debug }))

if (opts?.http?.cors) pipe.use(httpCorsMiddleware(opts?.http?.cors))
if (opts?.parser) pipe.use(validator({ parser: opts?.parser }))
if (opts?.parser) pipe.use(validator({ parser: opts?.parser, logger }))
if (opts?.function?.middleware) opts?.function?.middleware.forEach((p) => pipe.use(p))

pipe
Expand Down
5 changes: 2 additions & 3 deletions packages/api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { APIGatewayProxyStructuredResultV2, Context } from 'aws-lambda'
import { ApiParser } from './parsers'

// allow void responses only if expecting void body type (if parsing a response type, then we shouldn't allow void)
export type ApiResultV2<TBody> = TBody extends void ? void : ApiResultV2WithContent<TBody>
export type ApiResultV2WithContent<TBody> = Omit<APIGatewayProxyStructuredResultV2, 'body'> & {
body: TBody | undefined
export type ApiResultV2<TBody> = Omit<APIGatewayProxyStructuredResultV2, 'body'> & {
body?: TBody | undefined
}

export type ApiHandlerFunc<TResponse, TRequest, TPath, TQuery, TAuthorizer, TEvent> = ({
Expand Down
1 change: 0 additions & 1 deletion packages/api/test/data/api-proxy-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ export const payloadV2 = {
time: '12/Mar/2020:19:03:58 +0000',
timeEpoch: 1583348638390,
},
body: 'Hello from Lambda',
pathParameters: {
parameter1: 'value1',
},
Expand Down
6 changes: 2 additions & 4 deletions packages/api/test/events.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { describe, it } from 'vitest'
import { createApi } from '../src/api'
import { describe, it, expect } from 'vitest'
import { api } from '../src/api'
import * as events from './data/api-proxy-events'
import { ctx } from './data/lambda-context'

// TODO check specific differences of the formats

describe('Event formats', () => {
it('should support API proxy payload format v2', async () => {
const api = createApi()
const fn = api({
handler: async () => {
return {
Expand All @@ -24,7 +23,6 @@ describe('Event formats', () => {
})

it('should support API proxy payload format v1', async () => {
const api = createApi()
const fn = api({
handler: async ({ event }) => {
console.log(event)
Expand Down
65 changes: 65 additions & 0 deletions packages/api/test/handlers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, it, expect } from 'vitest'
import { api } from '../src/api'
import * as events from './data/api-proxy-events'
import { ctx } from './data/lambda-context'

describe('Handlers', () => {
it('should support an anonymous JSON body and stringify', async () => {
const event: any = {
...events.payloadV2,
body: JSON.stringify({
hi: 'test',
}),
}

const fn = api({
handler: () => ({
statusCode: 200,
body: {
hi: 'hello',
},
}),
})

const response = await fn(event, ctx())
expect(response.body).toBe('{"hi":"hello"}')
expect(response.statusCode).toBe(200)
})

it('should support an undefined body response', async () => {
const event: any = {
...events.payloadV2,
body: JSON.stringify({
hi: 'test',
}),
}

const fn = api({
handler: () => ({ statusCode: 200 }),
})
const response = await fn(event as any, ctx())
expect(response.statusCode).toBe(200)
})

it('should support a null body response', async () => {
const event: any = {
...events.payloadV2,
body: JSON.stringify({
hi: 'test',
}),
}

const fn = api({
handler: () => ({ statusCode: 200, body: null }),
})
const response = await fn(event as any, ctx())
expect(response.statusCode).toBe(200)
})

// supporting void makes the interface harder to use, so skipping this.
// it.skip('should support a void response', async () => {
// const fn = api({ handler: () => {} })
// const response = await fn(events.payloadV2 as any, ctx())
// expect(response.statusCode).toBe(200)
// })
})
33 changes: 33 additions & 0 deletions packages/api/test/profiling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, it, expect, vi } from 'vitest'
import { api } from '../src/api'
import { res } from '../src/res'
import * as events from './data/api-proxy-events'
import { ctx } from './data/lambda-context'
import { Logger } from '@funcy/core'

const sampleEvent = {
...events.payloadV2,
headers: {},
body: undefined,
}

describe('Monitoring -> Profiling', () => {
it('should not profile if not enabled', async () => {
const fn = api({ handler: () => res.ok() })
const response = await fn(sampleEvent as any, ctx())
console.log(response)
expect(response.statusCode).toBe(200)
})

it('should output profile information if enabled', async () => {
const logger = { debug: vi.fn(), error: vi.fn(), info: vi.fn() } as unknown as Logger
const fn = api({
monitoring: { logger: () => logger, enableProfiling: true },
handler: () => res.ok(),
})
const response = await fn(sampleEvent as any, ctx())
expect(response.statusCode).toBe(200)
expect(logger.info).toHaveBeenCalledWith('[Funcy] Profiling Enabled')
expect(logger.debug).toHaveBeenCalled()
})
})
2 changes: 1 addition & 1 deletion packages/api/test/res.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { res } from '../'
import { res } from '..'

describe('response helper (res)', () => {
it('should map a variable status code', () => {
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/middleware/profiler.plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import process from 'process'
import { Logger } from '../types'

const defaults = {
logger: console,
Expand Down Expand Up @@ -46,11 +47,11 @@ const memoryDiff = (a: MemorySnapshot, b: MemorySnapshot) => {
.join(' | ')
}

export const profiler = (opts = {}) => {
export const profiler = (opts: { logger?: Logger } = {}) => {
const { logger } = { ...defaults, ...opts }
const store = new Map<string, any>()

console.log('[Funcy] Profiling Enabled')
logger.info('[Funcy] Profiling Enabled')

const start = (id: string) => {
store.set(id, { time: process.hrtime.bigint(), mem: memorySnapshot() })
Expand All @@ -61,8 +62,8 @@ export const profiler = (opts = {}) => {
const mem = memorySnapshot()

const time = Number.parseInt((process.hrtime.bigint() - item.time).toString()) / 1000000
logger.debug(id, time, 'ms')
logger.debug(id, memoryDiff(item.mem, mem))
logger.debug(`${id} - ${time}ms`)
logger.debug(`${id} - ${memoryDiff(item.mem, mem)}`)
store.delete(id)
}

Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ export interface FuncyOptions<TEvent, TResponse> {

/**
* The log level.
* Debug will log all events and responses.
* 'debug' will log all events and responses.
*
* @default 'info'
*/
logLevel?: 'trace' | 'info' | 'warn' | 'error'
logLevel?: 'debug' | 'info' | 'warn' | 'error'

/**
* If true, will enable memory and stopwatch profiling of the pipeline.
Expand Down Expand Up @@ -79,7 +79,7 @@ export interface FuncyOptions<TEvent, TResponse> {
}

export interface Logger {
trace: (message: any) => void
debug: (message: any) => void
info: (message: any) => void
warn: (message: any) => void
error: (message: any) => void
Expand Down

0 comments on commit 999376e

Please sign in to comment.