logixlysia 3.6.0 → 3.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.6.2](https://github.com/PunGrumpy/logixlysia/compare/v3.6.1...v3.6.2) (2024-09-18)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **logtofile:** store error to log file ([1bb2d07](https://github.com/PunGrumpy/logixlysia/commit/1bb2d07ae0c815742a9eb7c930ac8487ac287b2c)), closes [#63](https://github.com/PunGrumpy/logixlysia/issues/63)
9
+
10
+ ## [3.6.1](https://github.com/PunGrumpy/logixlysia/compare/v3.6.0...v3.6.1) (2024-08-21)
11
+
12
+
13
+ ### Performance Improvements
14
+
15
+ * **logger:** improve performance and fix TypeScript errors ([d3ed751](https://github.com/PunGrumpy/logixlysia/commit/d3ed751041443b9bd2ce53350994e1443df40971))
16
+
3
17
  ## [3.6.0](https://github.com/PunGrumpy/logixlysia/compare/v3.5.0...v3.6.0) (2024-07-24)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logixlysia",
3
- "version": "3.6.0",
3
+ "version": "3.6.2",
4
4
  "description": "🦊 Logixlysia is a logger for Elysia",
5
5
  "type": "module",
6
6
  "module": "src/index.ts",
@@ -15,10 +15,10 @@
15
15
  },
16
16
  "license": "MIT",
17
17
  "scripts": {
18
- "lint": "eslint ..",
19
- "lint:fix": "eslint .. --fix",
18
+ "lint": "eslint .",
19
+ "lint:fix": "eslint . --fix",
20
20
  "test": "bun test --timeout 5000 --coverage",
21
- "test:ci": "bun test --timeout 5000 --coverage",
21
+ "test:ci": "bun test --timeout 5000 --coverage --coverage-reporter=lcov",
22
22
  "publish": "npm publish",
23
23
  "dev": "bun run --watch example/basic.ts",
24
24
  "prepare": "husky",
@@ -75,19 +75,20 @@
75
75
  },
76
76
  "devDependencies": {
77
77
  "@elysiajs/eden": "^1.1.1",
78
- "@eslint/js": "^9.7.0",
78
+ "@eslint/js": "^9.8.0",
79
79
  "@trunkio/launcher": "^1.3.1",
80
- "@typescript-eslint/eslint-plugin": "^7.17.0",
81
- "@typescript-eslint/parser": "^7.17.0",
80
+ "@typescript-eslint/eslint-plugin": "^8.0.1",
81
+ "@typescript-eslint/parser": "^8.0.1",
82
82
  "bun-types": "^1.1.20",
83
83
  "commitizen": "^4.3.0",
84
84
  "cz-conventional-changelog": "^3.3.0",
85
- "eslint": "^9.7.0",
85
+ "eslint": "9.x",
86
86
  "eslint-plugin-simple-import-sort": "^12.1.1",
87
+ "globals": "^15.9.0",
87
88
  "husky": "^9.1.1",
88
89
  "lint-staged": "^15.2.7",
89
90
  "prettier": "^3.3.3",
90
- "typescript-eslint": "^7.17.0"
91
+ "typescript-eslint": "^8.0.1"
91
92
  },
92
93
  "peerDependencies": {
93
94
  "typescript": "^5.2.2"
@@ -0,0 +1,64 @@
1
+ import chalk from 'chalk'
2
+
3
+ import {
4
+ LogComponents,
5
+ LogData,
6
+ LogLevel,
7
+ Options,
8
+ RequestInfo,
9
+ StoreData
10
+ } from '../types'
11
+ import {
12
+ durationString,
13
+ logString,
14
+ methodString,
15
+ pathString,
16
+ statusString
17
+ } from '../utils'
18
+
19
+ const defaultLogFormat =
20
+ '🦊 {now} {level} {duration} {method} {pathname} {status} {message} {ip}'
21
+
22
+ function shouldUseColors(useColors: boolean, options?: Options): boolean {
23
+ if (options?.config?.useColors !== undefined) {
24
+ return options.config.useColors && process.env.NO_COLOR === undefined
25
+ }
26
+ return useColors && process.env.NO_COLOR === undefined
27
+ }
28
+
29
+ export function buildLogMessage(
30
+ level: LogLevel,
31
+ request: RequestInfo,
32
+ data: LogData,
33
+ store: StoreData,
34
+ options?: Options,
35
+ useColors: boolean = true
36
+ ): string {
37
+ const actuallyUseColors = shouldUseColors(useColors, options)
38
+ const now = new Date()
39
+ const components: LogComponents = {
40
+ now: actuallyUseColors
41
+ ? chalk.bgYellow(chalk.black(now.toLocaleString()))
42
+ : now.toLocaleString(),
43
+ epoch: Math.floor(now.getTime() / 1000).toString(),
44
+ level: logString(level, actuallyUseColors),
45
+ duration: durationString(store.beforeTime, actuallyUseColors),
46
+ method: methodString(request.method, actuallyUseColors),
47
+ pathname: pathString(request),
48
+ status: statusString(data.status || 200, actuallyUseColors),
49
+ message: data.message || '',
50
+ ip:
51
+ options?.config?.ip && request.headers.get('x-forwarded-for')
52
+ ? `IP: ${request.headers.get('x-forwarded-for')}`
53
+ : ''
54
+ }
55
+
56
+ const logFormat = options?.config?.customLogFormat || defaultLogFormat
57
+
58
+ return logFormat.replace(/{(\w+)}/g, (_, key: string) => {
59
+ if (key in components) {
60
+ return components[key as keyof LogComponents] || ''
61
+ }
62
+ return ''
63
+ })
64
+ }
@@ -0,0 +1,57 @@
1
+ import { logToTransports } from '../transports'
2
+ import { logToFile } from '../transports'
3
+ import {
4
+ LogData,
5
+ Logger,
6
+ LogLevel,
7
+ Options,
8
+ RequestInfo,
9
+ StoreData
10
+ } from '../types'
11
+ import { buildLogMessage } from './buildLogMessage'
12
+ import { filterLog } from './filter'
13
+ import { handleHttpError } from './handleHttpError'
14
+
15
+ async function log(
16
+ level: LogLevel,
17
+ request: RequestInfo,
18
+ data: LogData,
19
+ store: StoreData,
20
+ options?: Options
21
+ ): Promise<void> {
22
+ if (!filterLog(level, data.status || 200, request.method, options)) return
23
+
24
+ const logMessage = buildLogMessage(level, request, data, store, options, true)
25
+ console.log(logMessage)
26
+
27
+ const promises = []
28
+
29
+ if (options?.config?.logFilePath) {
30
+ promises.push(
31
+ logToFile(
32
+ options.config.logFilePath,
33
+ level,
34
+ request,
35
+ data,
36
+ store,
37
+ options
38
+ )
39
+ )
40
+ }
41
+
42
+ if (options?.config?.transports?.length) {
43
+ promises.push(logToTransports(level, request, data, store, options))
44
+ }
45
+
46
+ await Promise.all(promises)
47
+ }
48
+
49
+ export function createLogger(options?: Options): Logger {
50
+ return {
51
+ log: (level, request, data, store) =>
52
+ log(level, request, data, store, options),
53
+ handleHttpError: (request, error, store) =>
54
+ handleHttpError(request, error, store, options),
55
+ customLogFormat: options?.config?.customLogFormat
56
+ }
57
+ }
@@ -0,0 +1,22 @@
1
+ import { LogLevel, Options } from '../types'
2
+
3
+ const checkFilter = (filterValue: any, value: any) =>
4
+ Array.isArray(filterValue)
5
+ ? filterValue.includes(value)
6
+ : filterValue === value
7
+
8
+ export function filterLog(
9
+ logLevel: LogLevel,
10
+ status: number,
11
+ method: string,
12
+ options?: Options
13
+ ): boolean {
14
+ const filter = options?.config?.logFilter
15
+ if (!filter) return true
16
+
17
+ return (
18
+ (!filter.level || checkFilter(filter.level, logLevel)) &&
19
+ (!filter.status || checkFilter(filter.status, status)) &&
20
+ (!filter.method || checkFilter(filter.method, method))
21
+ )
22
+ }
@@ -0,0 +1,30 @@
1
+ import { logToFile } from '../transports'
2
+ import { HttpError, Options, RequestInfo, StoreData } from '../types'
3
+ import { buildLogMessage } from './buildLogMessage'
4
+
5
+ export function handleHttpError(
6
+ request: RequestInfo,
7
+ error: HttpError,
8
+ store: StoreData,
9
+ options?: Options
10
+ ): void {
11
+ const statusCode = error.status || 500
12
+ console.error(
13
+ buildLogMessage('ERROR', request, { status: statusCode }, store, options)
14
+ )
15
+
16
+ const promises = []
17
+
18
+ if (options?.config?.logFilePath) {
19
+ promises.push(
20
+ logToFile(
21
+ options.config.logFilePath,
22
+ 'ERROR',
23
+ request,
24
+ { status: statusCode },
25
+ store,
26
+ options
27
+ )
28
+ )
29
+ }
30
+ }
package/src/index.ts CHANGED
@@ -1,24 +1,9 @@
1
- import { Server } from 'bun'
2
- import Elysia from 'elysia'
1
+ import { Elysia } from 'elysia'
3
2
 
4
- import { createLogger, handleHttpError } from './logger'
5
- import { HttpError, Options } from './types'
6
- import startServer from './utils/start'
3
+ import { createLogger } from './core'
4
+ import { startServer } from './plugins'
5
+ import { HttpError, Options, Server } from './types'
7
6
 
8
- /**
9
- * Creates a logger plugin for ElysiaJS.
10
- *
11
- * @export
12
- * @module logger
13
- * @category Logger
14
- * @subcategory Functions
15
- *
16
- * @name Logixlysia
17
- * @description Logixlysia is a logger plugin for ElysiaJS.
18
- * @param {Options} [options] Configuration options for the logger.
19
- *
20
- * @returns {Elysia} The logger plugin for ElysiaJS.
21
- */
22
7
  export default function logixlysia(options?: Options): Elysia {
23
8
  const log = createLogger(options)
24
9
 
@@ -33,11 +18,14 @@ export default function logixlysia(options?: Options): Elysia {
33
18
  log.log('INFO', request, { status: 200 }, store as { beforeTime: bigint })
34
19
  })
35
20
  .onError({ as: 'global' }, ({ request, error, store }) => {
36
- handleHttpError(
21
+ log.handleHttpError(
37
22
  request,
38
23
  error as HttpError,
39
- store as { beforeTime: bigint },
40
- options
24
+ store as { beforeTime: bigint }
41
25
  )
42
26
  })
43
27
  }
28
+
29
+ export { createLogger } from './core'
30
+ export { handleHttpError } from './core'
31
+ export { logToTransports } from './transports'
@@ -0,0 +1 @@
1
+ export { default as startServer } from './startServer'
@@ -1,25 +1,12 @@
1
- import { Options, Server } from '~/types'
1
+ import { Options, Server } from '../types'
2
2
 
3
- /**
4
- * Creates a box text.
5
- *
6
- * @param {string} text The text.
7
- * @param {number} width The box width.
8
- * @returns {string} The box text.
9
- */
10
- function createBoxText(text: string, width: number): string {
3
+ const createBoxText = (text: string, width: number): string => {
11
4
  const paddingLength = Math.max(0, (width - text.length) / 2)
12
5
  const padding = ' '.repeat(paddingLength)
13
6
  return `${padding}${text}${padding}`.padEnd(width)
14
7
  }
15
8
 
16
- /**
17
- * Starts the server string.
18
- *
19
- * @param {Server} config The server configuration.
20
- * @returns {void} The server string.
21
- */
22
- function startServer(config: Server, options?: Options): void {
9
+ export default function startServer(config: Server, options?: Options): void {
23
10
  const { hostname, port, protocol } = config
24
11
  const showBanner = options?.config?.showBanner ?? true
25
12
 
@@ -29,19 +16,18 @@ function startServer(config: Server, options?: Options): void {
29
16
  const message = `🦊 Elysia is running at ${protocol}://${hostname}:${port}`
30
17
  const boxWidth = Math.max(title.length, message.length) + 4
31
18
  const border = '─'.repeat(boxWidth)
19
+ const emptyLine = createBoxText('', boxWidth)
32
20
 
33
21
  console.log(`
34
22
  ┌${border}┐
35
- │${createBoxText('', boxWidth)}│
23
+ │${emptyLine}│
36
24
  │${createBoxText(title, boxWidth)}│
37
- │${createBoxText('', boxWidth)}│
25
+ │${emptyLine}│
38
26
  │${createBoxText(message, boxWidth)}│
39
- │${createBoxText('', boxWidth)}│
27
+ │${emptyLine}│
40
28
  └${border}┘
41
29
  `)
42
30
  } else {
43
31
  console.log(`🦊 Elysia is running at ${protocol}://${hostname}:${port}`)
44
32
  }
45
33
  }
46
-
47
- export default startServer
@@ -0,0 +1,22 @@
1
+ import { buildLogMessage } from '../core/buildLogMessage'
2
+ import { LogData, LogLevel, Options, RequestInfo, StoreData } from '../types'
3
+
4
+ export async function logToTransports(
5
+ level: LogLevel,
6
+ request: RequestInfo,
7
+ data: LogData,
8
+ store: StoreData,
9
+ options?: Options
10
+ ): Promise<void> {
11
+ if (!options?.config?.transports || options.config.transports.length === 0) {
12
+ return
13
+ }
14
+
15
+ const message = buildLogMessage(level, request, data, store, options, false)
16
+
17
+ const promises = options.config.transports.map(transport =>
18
+ transport.log(level, message, { request, data, store })
19
+ )
20
+
21
+ await Promise.all(promises)
22
+ }
@@ -0,0 +1,29 @@
1
+ import { promises as fs } from 'fs'
2
+ import { dirname } from 'path'
3
+
4
+ import { buildLogMessage } from '../core/buildLogMessage'
5
+ import { LogData, LogLevel, Options, RequestInfo, StoreData } from '../types'
6
+
7
+ const dirCache = new Set<string>()
8
+
9
+ async function ensureDirectoryExists(filePath: string): Promise<void> {
10
+ const dir = dirname(filePath)
11
+ if (!dirCache.has(dir)) {
12
+ await fs.mkdir(dir, { recursive: true })
13
+ dirCache.add(dir)
14
+ }
15
+ }
16
+
17
+ export async function logToFile(
18
+ filePath: string,
19
+ level: LogLevel,
20
+ request: RequestInfo,
21
+ data: LogData,
22
+ store: StoreData,
23
+ options?: Options
24
+ ): Promise<void> {
25
+ await ensureDirectoryExists(filePath)
26
+ const logMessage =
27
+ buildLogMessage(level, request, data, store, options, false) + '\n'
28
+ await fs.appendFile(filePath, logMessage, { flag: 'a' })
29
+ }
@@ -1,22 +1,2 @@
1
- import { buildLogMessage } from '~/logger/buildLogMessage'
2
- import { LogData, LogLevel, Options, RequestInfo, StoreData } from '~/types'
3
-
4
- export async function logToTransports(
5
- level: LogLevel,
6
- request: RequestInfo,
7
- data: LogData,
8
- store: StoreData,
9
- options?: Options
10
- ): Promise<void> {
11
- if (!options?.config?.transports || options.config.transports.length === 0) {
12
- return
13
- }
14
-
15
- const message = buildLogMessage(level, request, data, store, options, false)
16
-
17
- const promises = options.config.transports.map(transport =>
18
- transport.log(level, message, { request, data, store })
19
- )
20
-
21
- await Promise.all(promises)
22
- }
1
+ export { logToTransports } from './console'
2
+ export { logToFile } from './file'
@@ -1,41 +1,58 @@
1
- interface RequestInfo {
1
+ export interface RequestInfo {
2
2
  headers: { get: (key: string) => string | null }
3
3
  method: string
4
4
  url: string
5
5
  }
6
6
 
7
- interface Server {
7
+ export interface Server {
8
8
  hostname?: string
9
9
  port?: number
10
10
  protocol?: string
11
11
  }
12
12
 
13
- interface ColorMap {
13
+ export interface ColorMap {
14
14
  [key: string]: (str: string) => string
15
15
  }
16
16
 
17
- type LogLevel = 'INFO' | 'WARNING' | 'ERROR' | string
17
+ export type LogLevel = 'INFO' | 'WARNING' | 'ERROR' | string
18
18
 
19
- interface LogData {
19
+ export interface LogData {
20
20
  status?: number
21
21
  message?: string
22
22
  }
23
23
 
24
- interface Logger {
24
+ export interface Logger {
25
25
  log(
26
26
  level: LogLevel,
27
27
  request: RequestInfo,
28
28
  data: LogData,
29
29
  store: StoreData
30
30
  ): void
31
+ handleHttpError(
32
+ request: RequestInfo,
33
+ error: HttpError,
34
+ store: StoreData
35
+ ): void
31
36
  customLogFormat?: string
32
37
  }
33
38
 
34
- interface StoreData {
39
+ export interface LogComponents {
40
+ now: string
41
+ epoch: string
42
+ level: string
43
+ duration: string
44
+ method: string
45
+ pathname: string | undefined
46
+ status: string
47
+ message: string
48
+ ip: string
49
+ }
50
+
51
+ export interface StoreData {
35
52
  beforeTime: bigint
36
53
  }
37
54
 
38
- class HttpError extends Error {
55
+ export class HttpError extends Error {
39
56
  status: number
40
57
 
41
58
  constructor(status: number, message: string) {
@@ -44,7 +61,7 @@ class HttpError extends Error {
44
61
  }
45
62
  }
46
63
 
47
- interface TransportFunction {
64
+ export interface TransportFunction {
48
65
  (
49
66
  level: LogLevel,
50
67
  message: string,
@@ -56,11 +73,11 @@ interface TransportFunction {
56
73
  ): Promise<void> | void
57
74
  }
58
75
 
59
- interface Transport {
76
+ export interface Transport {
60
77
  log: TransportFunction
61
78
  }
62
79
 
63
- interface Options {
80
+ export interface Options {
64
81
  config?: {
65
82
  customLogFormat?: string
66
83
  logFilePath?: string
@@ -70,21 +87,8 @@ interface Options {
70
87
  status?: number | number[]
71
88
  } | null
72
89
  ip?: boolean
90
+ useColors?: boolean
73
91
  showBanner?: boolean
74
92
  transports?: Transport[]
75
93
  }
76
94
  }
77
-
78
- export {
79
- ColorMap,
80
- HttpError,
81
- LogData,
82
- Logger,
83
- LogLevel,
84
- Options,
85
- RequestInfo,
86
- Server,
87
- StoreData,
88
- Transport,
89
- TransportFunction
90
- }
@@ -1,24 +1,14 @@
1
1
  import chalk from 'chalk'
2
2
 
3
- import { ColorMap } from '~/types'
3
+ import { ColorMap } from '../types'
4
4
 
5
- /**
6
- * The color map for the log levels.
7
- *
8
- * @type {ColorMap}
9
- */
10
- const LogLevelColorMap: ColorMap = {
5
+ export const LogLevelColorMap: ColorMap = {
11
6
  INFO: chalk.bgGreen.black,
12
7
  WARNING: chalk.bgYellow.black,
13
8
  ERROR: chalk.bgRed.black
14
9
  }
15
10
 
16
- /**
17
- * The color map for the HTTP methods.
18
- *
19
- * @type {ColorMap}
20
- */
21
- const HttpMethodColorMap: ColorMap = {
11
+ export const HttpMethodColorMap: ColorMap = {
22
12
  GET: chalk.green,
23
13
  POST: chalk.yellow,
24
14
  PUT: chalk.blue,
@@ -27,5 +17,3 @@ const HttpMethodColorMap: ColorMap = {
27
17
  HEAD: chalk.cyan,
28
18
  OPTIONS: chalk.magenta
29
19
  }
30
-
31
- export { HttpMethodColorMap, LogLevelColorMap }
@@ -1,45 +1,27 @@
1
1
  import chalk from 'chalk'
2
2
 
3
- /**
4
- * Converts a time difference into a formatted string with the most appropriate time unit.
5
- *
6
- * @param {bigint} beforeTime - The timestamp taken before the request.
7
- * @param {boolean} useColors - Whether to apply colors to the output.
8
- * @returns {string} A formatted duration string including the time unit.
9
- */
10
- function durationString(beforeTime: bigint, useColors: boolean): string {
11
- const currentTime = process.hrtime.bigint()
12
- const nanoseconds = Number(currentTime - beforeTime)
3
+ const timeUnits = [
4
+ { unit: 's', threshold: 1e9, decimalPlaces: 2 },
5
+ { unit: 'ms', threshold: 1e6, decimalPlaces: 0 },
6
+ { unit: 'µs', threshold: 1e3, decimalPlaces: 0 },
7
+ { unit: 'ns', threshold: 1, decimalPlaces: 0 }
8
+ ]
13
9
 
14
- const timeUnits = [
15
- { unit: 's', threshold: 1e9, decimalPlaces: 2 },
16
- { unit: 'ms', threshold: 1e6, decimalPlaces: 0 },
17
- { unit: 'µs', threshold: 1e3, decimalPlaces: 0 }
18
- ]
10
+ export default function durationString(
11
+ beforeTime: bigint,
12
+ useColors: boolean
13
+ ): string {
14
+ const nanoseconds = Number(process.hrtime.bigint() - beforeTime)
19
15
 
20
16
  for (const { unit, threshold, decimalPlaces } of timeUnits) {
21
17
  if (nanoseconds >= threshold) {
22
18
  const value = (nanoseconds / threshold).toFixed(decimalPlaces)
23
- return formatTime(value, unit, useColors)
19
+ const timeStr = `${value}${unit}`.padStart(8).padEnd(16)
20
+ return useColors ? chalk.gray(timeStr) : timeStr
24
21
  }
25
22
  }
26
23
 
27
- return formatTime(nanoseconds.toString(), 'ns', useColors)
28
- }
29
-
30
- /**
31
- * Formats the time value with the given unit and applies chalk styling.
32
- *
33
- * @param {string} value - The time value.
34
- * @param {string} unit - The time unit.
35
- * @param {boolean} useColors - Whether to apply colors to the output.
36
- * @returns {string} Styled time string.
37
- */
38
- function formatTime(value: string, unit: string, useColors: boolean): string {
39
- const timeStr = `${value}${unit}`
40
24
  return useColors
41
- ? chalk.gray(timeStr).padStart(8).padEnd(16)
42
- : timeStr.padStart(8).padEnd(16)
25
+ ? chalk.gray('0ns'.padStart(8).padEnd(16))
26
+ : '0ns'.padStart(8).padEnd(16)
43
27
  }
44
-
45
- export default durationString
@@ -0,0 +1,6 @@
1
+ export { HttpMethodColorMap, LogLevelColorMap } from './colorMapping'
2
+ export { default as durationString } from './duration'
3
+ export { default as logString } from './log'
4
+ export { default as methodString } from './method'
5
+ export { default as pathString } from './path'
6
+ export { default as statusString } from './status'
package/src/utils/log.ts CHANGED
@@ -1,19 +1,9 @@
1
- import { LogLevel } from '~/types'
2
-
1
+ import { LogLevel } from '../types'
3
2
  import { LogLevelColorMap } from './colorMapping'
4
3
 
5
- /**
6
- * Converts the log level to a string.
7
- *
8
- * @param {LogLevel} level The log level.
9
- * @param {boolean} useColors - Whether to apply colors to the output.
10
- * @returns {string} The log level as a string.
11
- */
12
- function logString(level: LogLevel, useColors: boolean): string {
4
+ export default function logString(level: LogLevel, useColors: boolean): string {
13
5
  const levelStr = level.toUpperCase()
14
6
  return useColors
15
7
  ? LogLevelColorMap[levelStr]?.(levelStr.padEnd(7)) || levelStr
16
8
  : levelStr.padEnd(7)
17
9
  }
18
-
19
- export default logString
@@ -1,17 +1,11 @@
1
1
  import { HttpMethodColorMap } from './colorMapping'
2
2
 
3
- /**
4
- * Converts an HTTP request method to a colored string representation.
5
- *
6
- * @param {string} method The HTTP request method (e.g., 'GET', 'POST').
7
- * @param {boolean} useColors - Whether to apply colors to the output.
8
- * @returns {string} A string representing the method.
9
- */
10
- function methodString(method: string, useColors: boolean): string {
3
+ export default function methodString(
4
+ method: string,
5
+ useColors: boolean
6
+ ): string {
11
7
  const colorFunction = HttpMethodColorMap[method]
12
8
  return useColors && colorFunction
13
9
  ? colorFunction(method.padEnd(7))
14
10
  : method.padEnd(7)
15
11
  }
16
-
17
- export default methodString
package/src/utils/path.ts CHANGED
@@ -1,17 +1,11 @@
1
- import { RequestInfo } from '~/types'
1
+ import { RequestInfo } from '../types'
2
2
 
3
- /**
4
- * Returns the path string.
5
- *
6
- * @param {RequestInfo} requestInfo The request info.
7
- * @returns {string | undefined} The path string.
8
- */
9
- function pathString(requestInfo: RequestInfo): string | undefined {
3
+ export default function pathString(
4
+ requestInfo: RequestInfo
5
+ ): string | undefined {
10
6
  try {
11
7
  return new URL(requestInfo.url).pathname
12
8
  } catch {
13
9
  return undefined
14
10
  }
15
11
  }
16
-
17
- export default pathString
@@ -1,13 +1,9 @@
1
1
  import chalk from 'chalk'
2
2
 
3
- /**
4
- * Converts the status code to a string.
5
- *
6
- * @param {number} status The status code.
7
- * @param {boolean} useColors - Whether to apply colors to the output.
8
- * @returns {string} The status code as a string.
9
- */
10
- function statusString(status: number, useColors: boolean): string {
3
+ export default function statusString(
4
+ status: number,
5
+ useColors: boolean
6
+ ): string {
11
7
  const color =
12
8
  status >= 500
13
9
  ? 'red'
@@ -20,5 +16,3 @@ function statusString(status: number, useColors: boolean): string {
20
16
  : 'white'
21
17
  return useColors ? chalk[color](status.toString()) : status.toString()
22
18
  }
23
-
24
- export default statusString
@@ -0,0 +1,9 @@
1
+ import { mock } from 'bun:test'
2
+
3
+ export const mockAppend = mock(() => Promise.resolve())
4
+ export const mockMkdir = mock(() => Promise.resolve())
5
+
6
+ export const promises = {
7
+ appendFile: mockAppend,
8
+ mkdir: mockMkdir
9
+ }
@@ -0,0 +1,45 @@
1
+ import { expect, test } from 'bun:test'
2
+
3
+ import { buildLogMessage } from '../../src/core/buildLogMessage'
4
+ import { LogData, LogLevel, Options, StoreData } from '../../src/types'
5
+ import { createMockRequest } from '../helpers'
6
+
7
+ test('buildLogMessage', () => {
8
+ const level: LogLevel = 'INFO'
9
+ const request = createMockRequest()
10
+ const data: LogData = { status: 200, message: 'Test message' }
11
+ const store: StoreData = { beforeTime: BigInt(0) }
12
+ const options: Options = {
13
+ config: {
14
+ ip: true,
15
+ customLogFormat: '{level} {message} {ip}'
16
+ }
17
+ }
18
+
19
+ const message = buildLogMessage(level, request, data, store, options, false)
20
+ expect(message).toContain('INFO')
21
+ expect(message).toContain('Test message')
22
+ expect(message).toContain('127.0.0.1')
23
+
24
+ const colorMessage = buildLogMessage(
25
+ level,
26
+ request,
27
+ data,
28
+ store,
29
+ options,
30
+ true
31
+ )
32
+
33
+ expect(colorMessage).toContain('INFO')
34
+ expect(colorMessage).toContain('Test message')
35
+ expect(colorMessage).toContain('127.0.0.1')
36
+
37
+ const hasAnsiCodes = /\\x1B\[[0-9;]*m/.test(colorMessage)
38
+ if (hasAnsiCodes) {
39
+ expect(colorMessage).not.toBe(message)
40
+ } else {
41
+ console.warn(
42
+ 'No ANSI color codes detected. Colors might be disabled in this environment.'
43
+ )
44
+ }
45
+ })
@@ -0,0 +1,35 @@
1
+ import { expect, test } from 'bun:test'
2
+
3
+ import { filterLog } from '../../src/core/filter'
4
+ import { Options } from '../../src/types'
5
+
6
+ test('filterLog', () => {
7
+ const options: Options = {
8
+ config: {
9
+ logFilter: {
10
+ level: 'ERROR',
11
+ method: 'POST',
12
+ status: 500
13
+ }
14
+ }
15
+ }
16
+
17
+ expect(filterLog('ERROR', 500, 'POST', options)).toBe(true)
18
+ expect(filterLog('INFO', 200, 'GET', options)).toBe(false)
19
+ expect(filterLog('WARNING', 400, 'PUT', options)).toBe(false)
20
+
21
+ // Test with array filters
22
+ const arrayOptions: Options = {
23
+ config: {
24
+ logFilter: {
25
+ level: ['ERROR', 'WARNING'],
26
+ method: ['POST', 'PUT'],
27
+ status: [500, 400]
28
+ }
29
+ }
30
+ }
31
+
32
+ expect(filterLog('ERROR', 500, 'POST', arrayOptions)).toBe(true)
33
+ expect(filterLog('WARNING', 400, 'PUT', arrayOptions)).toBe(true)
34
+ expect(filterLog('INFO', 200, 'GET', arrayOptions)).toBe(false)
35
+ })
@@ -0,0 +1,19 @@
1
+ import { mock } from 'bun:test'
2
+
3
+ import { RequestInfo } from '../src/types'
4
+
5
+ export function createMockRequest(
6
+ method: string = 'GET',
7
+ url: string = 'http://localhost:3000/'
8
+ ): RequestInfo {
9
+ return {
10
+ headers: {
11
+ get: (key: string) => (key === 'x-forwarded-for' ? '127.0.0.1' : null)
12
+ },
13
+ method,
14
+ url
15
+ }
16
+ }
17
+
18
+ export const mockConsoleLog = mock(() => {})
19
+ export const mockConsoleError = mock(() => {})
@@ -0,0 +1,8 @@
1
+ import { expect, test } from 'bun:test'
2
+
3
+ import logixlysia from '../src/index'
4
+
5
+ test('logixlysia', () => {
6
+ const elysia = logixlysia()
7
+ expect(elysia).toBeDefined()
8
+ })
@@ -0,0 +1,29 @@
1
+ import { expect, mock, test } from 'bun:test'
2
+
3
+ import { logToTransports } from '../../src/transports'
4
+ import { LogData, LogLevel, Options, StoreData } from '../../src/types'
5
+ import { createMockRequest } from '../helpers'
6
+
7
+ test('logToTransports', async () => {
8
+ const level: LogLevel = 'INFO'
9
+ const request = createMockRequest()
10
+ const data: LogData = { status: 200, message: 'Test message' }
11
+ const store: StoreData = { beforeTime: BigInt(0) }
12
+
13
+ const mockTransport = {
14
+ log: mock(() => Promise.resolve())
15
+ }
16
+
17
+ const options: Options = {
18
+ config: {
19
+ transports: [mockTransport]
20
+ }
21
+ }
22
+
23
+ await logToTransports(level, request, data, store, options)
24
+ expect(mockTransport.log).toHaveBeenCalled()
25
+
26
+ // Test with no transports
27
+ await logToTransports(level, request, data, store, {})
28
+ expect(mockTransport.log).toHaveBeenCalledTimes(1) // Should not be called again
29
+ })
package/tsconfig.json CHANGED
@@ -6,9 +6,6 @@
6
6
  "allowImportingTsExtensions": true,
7
7
  "module": "ESNext",
8
8
  "moduleResolution": "Bundler",
9
- "paths": {
10
- "~/*": ["./src/*"]
11
- },
12
9
  "resolveJsonModule": true,
13
10
  "types": ["bun-types"],
14
11
  "downlevelIteration": true,
@@ -1,59 +0,0 @@
1
- import chalk from 'chalk'
2
-
3
- import { LogData, LogLevel, Options, RequestInfo, StoreData } from '~/types'
4
- import durationString from '~/utils/duration'
5
- import logString from '~/utils/log'
6
- import methodString from '~/utils/method'
7
- import pathString from '~/utils/path'
8
- import statusString from '~/utils/status'
9
-
10
- /**
11
- * Builds a log message.
12
- *
13
- * @param {LogLevel} level The log level.
14
- * @param {RequestInfo} request The request information.
15
- * @param {LogData} data The log data.
16
- * @param {StoreData} store The store data.
17
- * @param {Options} options The logger options.
18
- * @param {boolean} useColors Whether to apply colors to the log message.
19
- * @returns {string} The formatted log message.
20
- */
21
- export function buildLogMessage(
22
- level: LogLevel,
23
- request: RequestInfo,
24
- data: LogData,
25
- store: StoreData,
26
- options?: Options,
27
- useColors: boolean = true
28
- ): string {
29
- const now = new Date()
30
- const nowStr = useColors
31
- ? chalk.bgYellow(chalk.black(now.toLocaleString()))
32
- : now.toLocaleString()
33
- const epochStr = Math.floor(now.getTime() / 1000).toString()
34
- const levelStr = logString(level, useColors)
35
- const durationStr = durationString(store.beforeTime, useColors)
36
- const methodStr = methodString(request.method, useColors)
37
- const pathnameStr = pathString(request)
38
- const statusStr = statusString(data.status || 200, useColors)
39
- const messageStr = data.message || ''
40
- const ipStr =
41
- options?.config?.ip && request.headers.get('x-forwarded-for')
42
- ? `IP: ${request.headers.get('x-forwarded-for')}`
43
- : ''
44
-
45
- const logFormat =
46
- options?.config?.customLogFormat ||
47
- '🦊 {now} {level} {duration} {method} {pathname} {status} {message} {ip}'
48
-
49
- return logFormat
50
- .replace('{now}', nowStr)
51
- .replace('{epoch}', epochStr)
52
- .replace('{level}', levelStr)
53
- .replace('{duration}', durationStr)
54
- .replace('{method}', methodStr)
55
- .replace('{pathname}', pathnameStr || '')
56
- .replace('{status}', statusStr)
57
- .replace('{message}', messageStr)
58
- .replace('{ip}', ipStr || '')
59
- }
@@ -1,62 +0,0 @@
1
- import { logToTransports } from '~/transports'
2
- import {
3
- LogData,
4
- Logger,
5
- LogLevel,
6
- Options,
7
- RequestInfo,
8
- StoreData
9
- } from '~/types'
10
-
11
- import { buildLogMessage } from './buildLogMessage'
12
- import { filterLog } from './filter'
13
- import { logToFile } from './logToFile'
14
-
15
- /**
16
- * Logs a message to the console and optionally to a file.
17
- *
18
- * @param {LogLevel} level The log level.
19
- * @param {RequestInfo} request The request information.
20
- * @param {LogData} data The log data.
21
- * @param {StoreData} store The store data.
22
- * @param {Options} options The logger options.
23
- */
24
- async function log(
25
- level: LogLevel,
26
- request: RequestInfo,
27
- data: LogData,
28
- store: StoreData,
29
- options?: Options
30
- ): Promise<void> {
31
- if (!filterLog(level, data.status || 200, request.method, options)) return
32
-
33
- const logMessage = buildLogMessage(level, request, data, store, options, true)
34
- console.log(logMessage)
35
-
36
- if (options?.config?.logFilePath) {
37
- await logToFile(
38
- options.config.logFilePath,
39
- level,
40
- request,
41
- data,
42
- store,
43
- options
44
- )
45
- }
46
-
47
- await logToTransports(level, request, data, store, options)
48
- }
49
-
50
- /**
51
- * Creates a logger instance.
52
- *
53
- * @param {Options} options The logger options.
54
- * @returns {Logger} The logger instance.
55
- */
56
- export function createLogger(options?: Options): Logger {
57
- return {
58
- log: async (level, request, data, store) =>
59
- log(level, request, data, store, options),
60
- customLogFormat: options?.config?.customLogFormat
61
- }
62
- }
@@ -1,31 +0,0 @@
1
- import { LogLevel, Options } from '~/types'
2
-
3
- /**
4
- * Filters log messages.
5
- *
6
- * @param {LogLevel} logLevel The log level.
7
- * @param {number} status The status code.
8
- * @param {string} method The method.
9
- * @param {Options} options The options.
10
- * @returns {boolean} `true` if the log message should be logged, otherwise `false`.
11
- */
12
- export function filterLog(
13
- logLevel: LogLevel,
14
- status: number,
15
- method: string,
16
- options?: Options
17
- ): boolean {
18
- const filter = options?.config?.logFilter
19
- if (!filter) return true
20
-
21
- const checkFilter = (filterValue: any, value: any) =>
22
- Array.isArray(filterValue)
23
- ? filterValue.includes(value)
24
- : filterValue === value
25
-
26
- return (
27
- (!filter.level || checkFilter(filter.level, logLevel)) &&
28
- (!filter.status || checkFilter(filter.status, status)) &&
29
- (!filter.method || checkFilter(filter.method, method))
30
- )
31
- }
@@ -1,28 +0,0 @@
1
- import { HttpError, Options, RequestInfo, StoreData } from '~/types'
2
-
3
- import { buildLogMessage } from './buildLogMessage'
4
-
5
- /**
6
- * Handles an HTTP error and logs it.
7
- *
8
- * @param {RequestInfo} request The request information.
9
- * @param {HttpError} error The HTTP error.
10
- * @param {StoreData} store The store data.
11
- * @param {Options} options The logger options.
12
- */
13
- export async function handleHttpError(
14
- request: RequestInfo,
15
- error: HttpError,
16
- store: StoreData,
17
- options?: Options
18
- ): Promise<void> {
19
- const statusCode = error.status || 500
20
- const logMessage = buildLogMessage(
21
- 'ERROR',
22
- request,
23
- { status: statusCode },
24
- store,
25
- options
26
- )
27
- console.error(logMessage)
28
- }
@@ -1,40 +0,0 @@
1
- import { promises as fs } from 'fs'
2
- import { dirname } from 'path'
3
-
4
- import { LogData, LogLevel, Options, RequestInfo, StoreData } from '~/types'
5
-
6
- import { buildLogMessage } from './buildLogMessage'
7
-
8
- /**
9
- * Ensures that the directory exists. If not, it creates the directory.
10
- *
11
- * @param {string} filePath The path to the log file.
12
- */
13
- async function ensureDirectoryExists(filePath: string): Promise<void> {
14
- const dir = dirname(filePath)
15
- await fs.mkdir(dir, { recursive: true })
16
- }
17
-
18
- /**
19
- * Logs a message to a file.
20
- *
21
- * @param {string} filePath The path to the log file.
22
- * @param {LogLevel} level The log level.
23
- * @param {RequestInfo} request The request information.
24
- * @param {LogData} data The log data.
25
- * @param {StoreData} store The store data.
26
- * @param {Options} options The logger options.
27
- */
28
- export async function logToFile(
29
- filePath: string,
30
- level: LogLevel,
31
- request: RequestInfo,
32
- data: LogData,
33
- store: StoreData,
34
- options?: Options
35
- ): Promise<void> {
36
- await ensureDirectoryExists(filePath)
37
- const logMessage =
38
- buildLogMessage(level, request, data, store, options, false) + '\n'
39
- await fs.appendFile(filePath, logMessage)
40
- }
File without changes