diff --git a/.changeset/structured-logger-init.md b/.changeset/structured-logger-init.md new file mode 100644 index 000000000..542f2e019 --- /dev/null +++ b/.changeset/structured-logger-init.md @@ -0,0 +1,5 @@ +--- +'@hono/structured-logger': minor +--- + +Add @hono/structured-logger middleware: library agnostic structured logging with request scoped logger on c.var.logger, automatic response time measurement, and native requestId integration. diff --git a/packages/structured-logger/README.md b/packages/structured-logger/README.md new file mode 100644 index 000000000..c78e15b69 --- /dev/null +++ b/packages/structured-logger/README.md @@ -0,0 +1,216 @@ +# @hono/structured-logger + +Structured Logger middleware for [Hono](https://hono.dev). + +Library agnostic: works with pino, winston, bunyan, console, or any logger that implements the `BaseLogger` interface. Zero dependencies. Provides a request scoped logger on `c.var.logger` with full type safety, automatic response time measurement, and native integration with `hono/request-id`. + +## Install + +```bash +npm install @hono/structured-logger +``` + +## Usage + +### With pino + +```typescript +import { Hono } from 'hono' +import { requestId } from 'hono/request-id' +import { structuredLogger } from '@hono/structured-logger' +import pino from 'pino' + +const rootLogger = pino() + +const app = new Hono() + +app.use(requestId()) +app.use( + structuredLogger({ + createLogger: (c) => rootLogger.child({ requestId: c.var.requestId }), + }) +) + +app.get('/', (c) => { + c.var.logger.info('handling request') + return c.text('Hello!') +}) +``` + +### With winston + +```typescript +import { Hono } from 'hono' +import { structuredLogger } from '@hono/structured-logger' +import winston from 'winston' + +const rootLogger = winston.createLogger({ + /* config */ +}) + +const app = new Hono() + +app.use( + structuredLogger({ + createLogger: (c) => rootLogger.child({ requestId: c.var.requestId }), + }) +) +``` + +### With console (development, zero deps) + +```typescript +import { Hono } from 'hono' +import { structuredLogger } from '@hono/structured-logger' + +const app = new Hono() + +app.use( + structuredLogger({ + createLogger: () => console, + }) +) +``` + +### Custom hooks + +```typescript +import { Hono } from 'hono' +import { structuredLogger } from '@hono/structured-logger' +import pino from 'pino' + +const rootLogger = pino() + +const app = new Hono() + +app.use( + structuredLogger({ + createLogger: (c) => rootLogger.child({ requestId: c.var.requestId }), + onRequest: (logger, c) => { + logger.info( + { + method: c.req.method, + path: c.req.path, + userAgent: c.req.header('user-agent'), + }, + 'incoming request' + ) + }, + onResponse: (logger, c, elapsedMs) => { + logger.info( + { + status: c.res.status, + elapsedMs, + contentLength: c.res.headers.get('content-length'), + }, + 'request completed' + ) + }, + onError: (logger, err, c) => { + logger.error( + { + err, + method: c.req.method, + path: c.req.path, + }, + 'request failed' + ) + }, + }) +) +``` + +### Custom context key + +If you already have a `logger` variable on your context, use `contextKey` to pick a different name: + +```typescript +app.use( + structuredLogger({ + createLogger: () => myLogger, + contextKey: 'log', + }) +) + +app.get('/', (c) => { + c.var.log.info('hello') + return c.text('ok') +}) +``` + +### Type safe context + +Pass your logger type as a generic to `Hono` for full type safety on `c.var.logger`: + +```typescript +import { Hono } from 'hono' +import { structuredLogger } from '@hono/structured-logger' +import pino from 'pino' + +const rootLogger = pino() + +const app = new Hono<{ + Variables: { + logger: ReturnType + } +}>() + +app.use( + structuredLogger({ + createLogger: (c) => rootLogger.child({ foo: 'bar' }), + }) +) + +app.get('/', (c) => { + c.var.logger.info('hello') // typed! + return c.json({ foo: 'bar' }) +}) +``` + +## API + +### `structuredLogger(options)` + +Returns a Hono `MiddlewareHandler`. + +#### Options + +| Option | Type | Required | Default | Description | +| -------------- | --------------------------------------------------------------------- | -------- | -------------------------------------------------------- | ----------------------------------------------------- | +| `createLogger` | `(c: Context) => L` | Yes | | Factory that creates a request scoped logger instance | +| `contextKey` | `string` | No | `'logger'` | Key used to store the logger on `c.var` | +| `onRequest` | `(logger: L, c: Context) => void \| Promise` | No | Logs method + path at info level | Called before handler execution | +| `onResponse` | `(logger: L, c: Context, elapsedMs: number) => void \| Promise` | No | Logs method, path, status and elapsed time at info level | Called after handler execution | +| `onError` | `(logger: L, err: Error, c: Context) => void \| Promise` | No | Logs error, method, path and status at error level | Called when handler throws | + +### `BaseLogger` + +Minimal interface your logger must implement: + +```typescript +interface BaseLogger { + info(obj: unknown, msg?: string, ...args: unknown[]): void + warn(obj: unknown, msg?: string, ...args: unknown[]): void + error(obj: unknown, msg?: string, ...args: unknown[]): void + debug(obj: unknown, msg?: string, ...args: unknown[]): void +} +``` + +Compatible with pino, winston, bunyan, console, and most logging libraries out of the box. + +## Behavior + +1. `createLogger(c)` is called once per request. +2. The logger is stored on `c.var[contextKey]`. +3. `onRequest` fires before handler execution. +4. After handler completes, `onResponse` fires with elapsed time in milliseconds (measured via `performance.now()`). +5. If the handler throws, Hono's error handler runs first, then `onError` fires (checking `c.error`). `onResponse` is skipped when an error occurred. +6. `onError` and `onResponse` are mutually exclusive per request. + +## Runtime compatibility + +Works on all runtimes supported by Hono: Node.js, Deno, Bun, Cloudflare Workers, AWS Lambda, Vercel Edge, Fastly Compute. No Node specific APIs used. + +## License + +MIT diff --git a/packages/structured-logger/deno.json b/packages/structured-logger/deno.json new file mode 100644 index 000000000..ad845cf72 --- /dev/null +++ b/packages/structured-logger/deno.json @@ -0,0 +1,15 @@ +{ + "name": "@hono/structured-logger", + "version": "0.0.0", + "license": "MIT", + "exports": { + ".": "./src/index.ts" + }, + "imports": { + "hono": "jsr:@hono/hono@^4.8.3" + }, + "publish": { + "include": ["deno.json", "README.md", "src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] + } +} diff --git a/packages/structured-logger/eslint-suppressions.json b/packages/structured-logger/eslint-suppressions.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/packages/structured-logger/eslint-suppressions.json @@ -0,0 +1 @@ +{} diff --git a/packages/structured-logger/package.json b/packages/structured-logger/package.json new file mode 100644 index 000000000..b4e424b0c --- /dev/null +++ b/packages/structured-logger/package.json @@ -0,0 +1,56 @@ +{ + "name": "@hono/structured-logger", + "version": "0.0.0", + "description": "Structured Logger middleware for Hono", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsdown", + "format": "prettier --check . --ignore-path ../../.gitignore", + "lint": "eslint", + "typecheck": "tsc -b tsconfig.json", + "test": "vitest", + "version:jsr": "yarn version:set $npm_package_version" + }, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "license": "MIT", + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public", + "provenance": true + }, + "repository": { + "type": "git", + "url": "git+https://github.com/honojs/middleware.git", + "directory": "packages/structured-logger" + }, + "homepage": "https://github.com/honojs/middleware", + "peerDependencies": { + "hono": ">=4.0.0" + }, + "devDependencies": { + "hono": "^4.11.5", + "tsdown": "^0.15.9", + "typescript": "^5.9.3", + "vitest": "^4.1.0-beta.1" + }, + "engines": { + "node": ">=16.0.0" + } +} diff --git a/packages/structured-logger/src/index.test.ts b/packages/structured-logger/src/index.test.ts new file mode 100644 index 000000000..012a748fc --- /dev/null +++ b/packages/structured-logger/src/index.test.ts @@ -0,0 +1,425 @@ +import { Hono } from 'hono' +import { describe, expect, it, vi } from 'vitest' +import type { BaseLogger } from './index' +import { structuredLogger } from './index' + +type MockLogger = { + [K in keyof BaseLogger]: BaseLogger[K] & ReturnType +} + +function createMockLogger(): MockLogger { + return { + info: vi.fn() as MockLogger['info'], + warn: vi.fn() as MockLogger['warn'], + error: vi.fn() as MockLogger['error'], + debug: vi.fn() as MockLogger['debug'], + } +} + +describe('structuredLogger', () => { + describe('core behavior', () => { + it('calls createLogger once per request with the context', async () => { + const mockLogger = createMockLogger() + const createLogger = vi.fn(() => mockLogger) + const app = new Hono() + + app.use(structuredLogger({ createLogger })) + app.get('/', (c) => c.text('ok')) + + await app.request('/') + + expect(createLogger).toHaveBeenCalledTimes(1) + expect(createLogger.mock.calls).toHaveLength(1) + expect((createLogger.mock.calls as unknown[][])[0]?.[0]).toBeDefined() + }) + + it('makes the logger accessible via c.var.logger in the handler', async () => { + const mockLogger = createMockLogger() + let capturedLogger: unknown = null + + const app = new Hono<{ Variables: { logger: BaseLogger } }>() + app.use(structuredLogger({ createLogger: () => mockLogger })) + app.get('/', (c) => { + capturedLogger = c.var.logger + return c.text('ok') + }) + + await app.request('/') + + expect(capturedLogger).toBe(mockLogger) + }) + + it('supports a custom contextKey', async () => { + const mockLogger = createMockLogger() + let capturedLogger: unknown = null + + const app = new Hono<{ Variables: { log: BaseLogger } }>() + app.use(structuredLogger({ createLogger: () => mockLogger, contextKey: 'log' })) + app.get('/', (c) => { + capturedLogger = c.var.log + return c.text('ok') + }) + + await app.request('/') + + expect(capturedLogger).toBe(mockLogger) + }) + + it('passes elapsedMs as a number >= 0 to onResponse', async () => { + const mockLogger = createMockLogger() + let capturedElapsed: number | null = null + + const app = new Hono() + app.use( + structuredLogger({ + createLogger: () => mockLogger, + onResponse: (_logger, _c, elapsedMs) => { + capturedElapsed = elapsedMs + }, + }) + ) + app.get('/', (c) => c.text('ok')) + + await app.request('/') + + expect(capturedElapsed).toBeTypeOf('number') + expect(capturedElapsed).toBeGreaterThanOrEqual(0) + }) + + it('executes in order: createLogger, onRequest, handler, onResponse', async () => { + const order: string[] = [] + const mockLogger = createMockLogger() + + const app = new Hono() + app.use( + structuredLogger({ + createLogger: () => { + order.push('createLogger') + return mockLogger + }, + onRequest: () => { + order.push('onRequest') + }, + onResponse: () => { + order.push('onResponse') + }, + }) + ) + app.get('/', (c) => { + order.push('handler') + return c.text('ok') + }) + + await app.request('/') + + expect(order).toEqual(['createLogger', 'onRequest', 'handler', 'onResponse']) + }) + }) + + describe('error handling', () => { + it('calls onError when the handler throws', async () => { + const mockLogger = createMockLogger() + const onError = vi.fn() + const handlerError = new Error('handler failed') + + const app = new Hono() + app.use(structuredLogger({ createLogger: () => mockLogger, onError })) + app.get('/', () => { + throw handlerError + }) + app.onError((_err, c) => c.text('error', 500)) + + await app.request('/') + + expect(onError).toHaveBeenCalledTimes(1) + expect(onError.mock.calls[0]?.[0]).toBe(mockLogger) + expect(onError.mock.calls[0]?.[1]).toBe(handlerError) + }) + + it('the error is still handled by app.onError', async () => { + const mockLogger = createMockLogger() + const handlerError = new Error('boom') + let caughtError: unknown = null + + const app = new Hono() + app.use(structuredLogger({ createLogger: () => mockLogger })) + app.get('/', () => { + throw handlerError + }) + app.onError((err, c) => { + caughtError = err + return c.text('error', 500) + }) + + await app.request('/') + + expect(caughtError).toBe(handlerError) + }) + + it('passes the error as an Error instance to onError', async () => { + const mockLogger = createMockLogger() + const onError = vi.fn() + + const app = new Hono() + app.use(structuredLogger({ createLogger: () => mockLogger, onError })) + app.get('/', () => { + throw new Error('typed error') + }) + app.onError((_err, c) => c.text('error', 500)) + + await app.request('/') + + expect(onError).toHaveBeenCalledTimes(1) + const errorArg = onError.mock.calls[0]?.[1] as Error + expect(errorArg).toBeInstanceOf(Error) + expect(errorArg.message).toBe('typed error') + }) + + it('does not call onResponse when the handler throws', async () => { + const mockLogger = createMockLogger() + const onResponse = vi.fn() + + const app = new Hono() + app.use(structuredLogger({ createLogger: () => mockLogger, onResponse })) + app.get('/', () => { + throw new Error('fail') + }) + app.onError((_err, c) => c.text('error', 500)) + + await app.request('/') + + expect(onResponse).not.toHaveBeenCalled() + }) + }) + + describe('default hooks', () => { + it('default onRequest logs method and path via logger.info', async () => { + const mockLogger = createMockLogger() + + const app = new Hono() + app.use(structuredLogger({ createLogger: () => mockLogger })) + app.get('/test', (c) => c.text('ok')) + + await app.request('/test') + + const infoCall = mockLogger.info.mock.calls[0] + expect(infoCall?.[0]).toEqual({ method: 'GET', path: '/test' }) + expect(infoCall?.[1]).toBe('request start') + }) + + it('default onResponse logs status and elapsed via logger.info', async () => { + const mockLogger = createMockLogger() + + const app = new Hono() + app.use(structuredLogger({ createLogger: () => mockLogger })) + app.get('/', (c) => c.text('ok')) + + await app.request('/') + + const lastInfoCall = mockLogger.info.mock.calls[mockLogger.info.mock.calls.length - 1] + expect(lastInfoCall?.[0]).toHaveProperty('method', 'GET') + expect(lastInfoCall?.[0]).toHaveProperty('path', '/') + expect(lastInfoCall?.[0]).toHaveProperty('status', 200) + expect(lastInfoCall?.[0]).toHaveProperty('elapsedMs') + expect(lastInfoCall?.[1]).toBe('request end') + }) + + it('default onError logs the error via logger.error', async () => { + const mockLogger = createMockLogger() + + const app = new Hono() + app.use(structuredLogger({ createLogger: () => mockLogger })) + app.get('/', () => { + throw new Error('something broke') + }) + app.onError((_err, c) => c.text('error', 500)) + + await app.request('/') + + expect(mockLogger.error).toHaveBeenCalledTimes(1) + const errorCall = mockLogger.error.mock.calls[0] + expect(errorCall?.[0]).toHaveProperty('err') + expect(errorCall?.[0]).toHaveProperty('method', 'GET') + expect(errorCall?.[0]).toHaveProperty('path', '/') + expect(errorCall?.[0]).toHaveProperty('status', 500) + expect(errorCall?.[1]).toBe('request error') + }) + }) + + describe('integration', () => { + it('requestId is accessible inside createLogger', async () => { + let capturedRequestId: string | undefined + + const app = new Hono<{ Variables: { requestId: string; logger: BaseLogger } }>() + + // Simulate requestId middleware + app.use(async (c, next) => { + c.set('requestId', 'test-req-id-123') + await next() + }) + + app.use( + structuredLogger({ + createLogger: (c) => { + capturedRequestId = c.var['requestId'] as string + return createMockLogger() + }, + }) + ) + app.get('/', (c) => c.text('ok')) + + await app.request('/') + + expect(capturedRequestId).toBe('test-req-id-123') + }) + + it('multiple instances on different paths do not interfere', async () => { + const apiLogger = createMockLogger() + const adminLogger = createMockLogger() + + const app = new Hono() + app.use('/api/*', structuredLogger({ createLogger: () => apiLogger })) + app.use('/admin/*', structuredLogger({ createLogger: () => adminLogger })) + + app.get('/api/data', (c) => c.text('api')) + app.get('/admin/panel', (c) => c.text('admin')) + + await app.request('/api/data') + expect(apiLogger.info).toHaveBeenCalled() + expect(adminLogger.info).not.toHaveBeenCalled() + + apiLogger.info.mockClear() + await app.request('/admin/panel') + expect(adminLogger.info).toHaveBeenCalled() + expect(apiLogger.info).not.toHaveBeenCalled() + }) + + it('async hooks are awaited correctly', async () => { + const order: string[] = [] + const mockLogger = createMockLogger() + + const app = new Hono() + app.use( + structuredLogger({ + createLogger: () => mockLogger, + onRequest: async () => { + await new Promise((r) => setTimeout(r, 10)) + order.push('async onRequest done') + }, + onResponse: async () => { + await new Promise((r) => setTimeout(r, 10)) + order.push('async onResponse done') + }, + }) + ) + app.get('/', (c) => { + order.push('handler') + return c.text('ok') + }) + + await app.request('/') + + expect(order).toEqual(['async onRequest done', 'handler', 'async onResponse done']) + }) + + it('concurrent requests get separate logger instances', async () => { + const loggers: BaseLogger[] = [] + + const app = new Hono() + app.use( + structuredLogger({ + createLogger: () => { + const logger = createMockLogger() + loggers.push(logger) + return logger + }, + }) + ) + app.get('/', (c) => c.text('ok')) + + await Promise.all([app.request('/'), app.request('/'), app.request('/')]) + + expect(loggers).toHaveLength(3) + expect(loggers[0]).not.toBe(loggers[1]) + expect(loggers[1]).not.toBe(loggers[2]) + }) + }) + + describe('edge cases', () => { + it('handles streaming responses without error', async () => { + const mockLogger = createMockLogger() + const onResponse = vi.fn() + + const app = new Hono() + app.use(structuredLogger({ createLogger: () => mockLogger, onResponse })) + app.get('/', (c) => { + return c.text('streamed content') + }) + + const res = await app.request('/') + + expect(res.status).toBe(200) + expect(onResponse).toHaveBeenCalledTimes(1) + }) + + it('propagates errors from createLogger', async () => { + const app = new Hono() + app.use( + structuredLogger({ + createLogger: () => { + throw new Error('factory failed') + }, + }) + ) + app.get('/', (c) => c.text('ok')) + app.onError((err, c) => c.text(err.message, 500)) + + const res = await app.request('/') + + expect(res.status).toBe(500) + expect(await res.text()).toBe('factory failed') + }) + + it('propagates errors from onRequest hook', async () => { + const mockLogger = createMockLogger() + + const app = new Hono() + app.use( + structuredLogger({ + createLogger: () => mockLogger, + onRequest: () => { + throw new Error('onRequest blew up') + }, + }) + ) + app.get('/', (c) => c.text('ok')) + app.onError((err, c) => c.text(err.message, 500)) + + const res = await app.request('/') + + expect(res.status).toBe(500) + expect(await res.text()).toBe('onRequest blew up') + }) + + it('propagates errors from onResponse hook', async () => { + const mockLogger = createMockLogger() + + const app = new Hono() + app.use( + structuredLogger({ + createLogger: () => mockLogger, + onResponse: () => { + throw new Error('onResponse blew up') + }, + }) + ) + app.get('/', (c) => c.text('ok')) + app.onError((err, c) => c.text(err.message, 500)) + + const res = await app.request('/') + + expect(res.status).toBe(500) + expect(await res.text()).toBe('onResponse blew up') + }) + }) +}) diff --git a/packages/structured-logger/src/index.ts b/packages/structured-logger/src/index.ts new file mode 100644 index 000000000..4e07f55b1 --- /dev/null +++ b/packages/structured-logger/src/index.ts @@ -0,0 +1,101 @@ +/** + * @module + * Structured Logger Middleware for Hono. + */ + +import type { Context, MiddlewareHandler } from 'hono' + +/** + * Minimal logger interface compatible with pino, winston, console, bunyan, etc. + */ +export interface BaseLogger { + info(obj: unknown, msg?: string, ...args: unknown[]): void + warn(obj: unknown, msg?: string, ...args: unknown[]): void + error(obj: unknown, msg?: string, ...args: unknown[]): void + debug(obj: unknown, msg?: string, ...args: unknown[]): void +} + +export interface StructuredLoggerOptions { + /** + * Factory function that creates a request scoped logger. + * Receives the Hono context so you can inject requestId, headers, etc. + */ + createLogger: (c: Context) => L + + /** + * Key used to store the logger instance on c.var. + * @default 'logger' + */ + contextKey?: string + + /** + * Called after logger creation, before handler execution. + * Use for logging request start, incoming headers, etc. + * Default: logs method + path at info level. + */ + onRequest?: (logger: L, c: Context) => void | Promise + + /** + * Called after handler execution with elapsed time in ms. + * Use for logging response status, duration, etc. + * Default: logs status + elapsed at info level. + */ + onResponse?: (logger: L, c: Context, elapsedMs: number) => void | Promise + + /** + * Called when an error occurs during handler execution. + * Default: logs error at error level. + */ + onError?: (logger: L, err: Error, c: Context) => void | Promise +} + +const now = typeof performance !== 'undefined' ? () => performance.now() : () => Date.now() + +function defaultOnRequest(logger: BaseLogger, c: Context): void { + logger.info({ method: c.req.method, path: c.req.path }, 'request start') +} + +function defaultOnResponse(logger: BaseLogger, c: Context, elapsedMs: number): void { + logger.info( + { method: c.req.method, path: c.req.path, status: c.res.status, elapsedMs }, + 'request end' + ) +} + +function defaultOnError(logger: BaseLogger, err: Error, c: Context): void { + logger.error( + { err, method: c.req.method, path: c.req.path, status: c.res.status }, + 'request error' + ) +} + +export function structuredLogger( + options: StructuredLoggerOptions +): MiddlewareHandler { + const { + createLogger, + contextKey = 'logger', + onRequest = defaultOnRequest, + onResponse = defaultOnResponse, + onError = defaultOnError, + } = options + + return async (c, next) => { + const logger = createLogger(c) + c.set(contextKey as never, logger as never) + + const start = now() + + await onRequest(logger, c) + + await next() + + const elapsed = now() - start + + if (c.error) { + await onError(logger, c.error instanceof Error ? c.error : new Error(String(c.error)), c) + } else { + await onResponse(logger, c, elapsed) + } + } +} diff --git a/packages/structured-logger/tsconfig.build.json b/packages/structured-logger/tsconfig.build.json new file mode 100644 index 000000000..4a1f19acc --- /dev/null +++ b/packages/structured-logger/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": {}, + "references": [] +} diff --git a/packages/structured-logger/tsconfig.json b/packages/structured-logger/tsconfig.json new file mode 100644 index 000000000..d4ad6cfa3 --- /dev/null +++ b/packages/structured-logger/tsconfig.json @@ -0,0 +1,12 @@ +{ + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.build.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/structured-logger/tsconfig.spec.json b/packages/structured-logger/tsconfig.spec.json new file mode 100644 index 000000000..89d58a521 --- /dev/null +++ b/packages/structured-logger/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noUnusedLocals": false, + "outDir": "../../dist/packages/structured-logger", + "types": ["vitest/globals"] + }, + "references": [] +} diff --git a/packages/structured-logger/tsdown.config.ts b/packages/structured-logger/tsdown.config.ts new file mode 100644 index 000000000..4baf13a49 --- /dev/null +++ b/packages/structured-logger/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + attw: true, + clean: true, + dts: true, + entry: 'src/index.ts', + format: ['cjs', 'esm'], + publint: true, + tsconfig: 'tsconfig.build.json', +}) diff --git a/packages/structured-logger/vitest.config.ts b/packages/structured-logger/vitest.config.ts new file mode 100644 index 000000000..19afc7637 --- /dev/null +++ b/packages/structured-logger/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineProject } from 'vitest/config' + +export default defineProject({ + test: { + globals: true, + include: ['src/**/*.test.ts'], + restoreMocks: true, + }, +}) diff --git a/tsconfig.json b/tsconfig.json index 49c282f45..52749834a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,6 +36,7 @@ { "path": "packages/session" }, { "path": "packages/ssg-plugins-essential" }, { "path": "packages/standard-validator" }, + { "path": "packages/structured-logger" }, { "path": "packages/stytch-auth" }, { "path": "packages/swagger-editor" }, { "path": "packages/swagger-ui" }, diff --git a/yarn.lock b/yarn.lock index 1c622bc9f..6917f4060 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2331,6 +2331,19 @@ __metadata: languageName: unknown linkType: soft +"@hono/structured-logger@workspace:packages/structured-logger": + version: 0.0.0-use.local + resolution: "@hono/structured-logger@workspace:packages/structured-logger" + dependencies: + hono: "npm:^4.11.5" + tsdown: "npm:^0.15.9" + typescript: "npm:^5.9.3" + vitest: "npm:^4.1.0-beta.1" + peerDependencies: + hono: ">=4.0.0" + languageName: unknown + linkType: soft + "@hono/stytch-auth@workspace:packages/stytch-auth": version: 0.0.0-use.local resolution: "@hono/stytch-auth@workspace:packages/stytch-auth"