├── .github └── workflows │ └── release.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── bun.lockb ├── example └── index.ts ├── package.json ├── scripts ├── build.ts ├── bump.ts └── release.ts ├── src ├── formatters.ts └── index.ts ├── tsconfig.dts.json └── tsconfig.json /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | bump: 7 | description: "Bump Type" 8 | required: true 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | - major 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | publish: 21 | permissions: 22 | contents: write 23 | id-token: write 24 | 25 | runs-on: ubuntu-latest 26 | timeout-minutes: 15 27 | 28 | steps: 29 | - name: Checkout Source Code 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Setup Bun 35 | uses: oven-sh/setup-bun@v1 36 | with: 37 | bun-version: latest 38 | 39 | - name: Install Dependencies 40 | run: bun install --frozen-lockfile 41 | 42 | - name: Set Git User 43 | run: | 44 | git config user.name "${GITHUB_ACTOR}" 45 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 46 | 47 | - name: Set NPM Config 48 | run: npm config set //registry.npmjs.org/:_authToken "${NPM_TOKEN}" 49 | env: 50 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | 52 | - name: Bump version 53 | run: bun run bump ${{ github.event.inputs.bump }} 54 | 55 | - name: Build 56 | run: bun run build 57 | 58 | - name: Publish 59 | run: npm publish --provenance --access public 60 | 61 | - name: Commit 62 | run: git push --follow-tags 63 | 64 | - name: Release Notes 65 | run: bunx changelogithub 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Build 5 | dist -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | /* 3 | 4 | # Except 5 | !scripts 6 | !src 7 | !example 8 | !README.md 9 | !.prettierrc 10 | !tsconfig.json 11 | !build.config.ts -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "printWidth": 80, 6 | "singleQuote": false, 7 | "arrowParens": "avoid", 8 | "importOrder": [ 9 | "", 10 | "", 11 | "", 12 | "", 13 | "^[./]" 14 | ], 15 | "plugins": ["@ianvs/prettier-plugin-sort-imports"] 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tanishq Manuja 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://raw.github.com/tanishqmanuja/static/main/banners/nice-logger.webp?maxAge=2592000) 2 | 3 |

"Not the nicest, but a pretty nice and sweet logger for Elysia."

4 | 5 |
6 | 7 | [![Downloads][downloads-shield]][npm-url] 8 | [![NPM Version][npm-shield]][npm-url] 9 | ![GitHub Workflow Status][ci-status-shield] 10 | [![License][license-shield]][license-url] 11 | 12 |
13 | 14 | 15 | 16 | [ci-status-shield]: https://img.shields.io/github/actions/workflow/status/tanishqmanuja/nice-logger/release.yaml?branch=main&style=for-the-badge&label=ci 17 | [downloads-shield]: https://img.shields.io/npm/dm/%40tqman%2Fnice-logger?style=for-the-badge 18 | [license-shield]: https://img.shields.io/github/license/tanishqmanuja/apkmirror-downloader?style=for-the-badge 19 | [license-url]: https://github.com/tanishqmanuja/nice-logger/blob/main/LICENSE.md 20 | [npm-shield]: https://img.shields.io/npm/v/@tqman/nice-logger?style=for-the-badge 21 | [npm-url]: https://www.npmjs.com/package/@tqman/nice-logger 22 | 23 | ## 🚀 Installation 24 | 25 | ```bash 26 | bun add @tqman/nice-logger 27 | ``` 28 | 29 | ## 📃 Usage 30 | 31 | ```ts 32 | import Elysia from "elysia"; 33 | import { logger } from "@tqman/nice-logger"; 34 | 35 | const app = new Elysia() 36 | .use(logger({ 37 | mode: "live", // "live" or "combined" (default: "combined") 38 | withTimestamp: true, // optional (default: false) 39 | })); 40 | .get("/", "Hello via Elysia!") 41 | .listen(3000); 42 | ``` 43 | 44 | ## 🍰 Showcase 45 | 46 | Some awesome projects to checkout !!! 47 | 48 | - [**todos-react-elysia**](https://github.com/tanishqmanuja/todos-react-elysia) - A simple starter fullstack todos app built with [React](https://react.dev) and [Elysia](https://elysiajs.com) that uses `@tqman/nice-logger`. 49 | 50 | - [**elysia-logger**](https://github.com/bogeychan/elysia-logger) - An [Elysia](https://elysiajs.com) logger plugin to use with [pino](https://github.com/pinojs/pino) library, developed by [@bogeychan](https://github.com/bogeychan). 51 | 52 | - [**logixlysia**](https://github.com/PunGrumpy/logixlysia) - Another great logger plugin for [Elysia](https://elysiajs.com) developed by [@PunGrumpy](https://github.com/PunGrumpy). 53 | 54 | ## 🍀 Show your Support 55 | 56 | Give a ⭐️ if this project helped you! 57 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanishqmanuja/nice-logger/a9578bbcfdf5800485ff17e5c0a582e079c52c1a/bun.lockb -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "@tqman/nice-logger"; 2 | import Elysia from "elysia"; 3 | 4 | function wait(time: number) { 5 | return new Promise(resolve => { 6 | setTimeout(() => { 7 | resolve(true); 8 | }, time); 9 | }); 10 | } 11 | 12 | const child = new Elysia({ prefix: "child" }) 13 | .get("/", "Hello via Child!") 14 | .get("/error", () => { 15 | throw new Error("Error via Child!"); 16 | }); 17 | 18 | const app = new Elysia() 19 | .use( 20 | logger({ 21 | mode: "live", 22 | withBanner: true, 23 | withTimestamp: true, 24 | }), 25 | ) 26 | .use(child) 27 | .get("/", "Hello via Elysia!") 28 | .get("/wait/:time", ({ params: { time } }) => 29 | wait(+time).then(() => `Waited ${time}ms`), 30 | ) 31 | .listen(3000); 32 | 33 | const req = (path: string) => new Request(`http://localhost:3000${path}`); 34 | 35 | await app.handle(req("/")); 36 | await app.handle(req("/child")); 37 | await app.handle(req("/none")); 38 | await app.handle(req("/child/error")); 39 | 40 | await Promise.all( 41 | [req("/wait/5000"), req("/child"), req("/wait/3000")].map(req => 42 | app.handle(req), 43 | ), 44 | ); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tqman/nice-logger", 3 | "version": "1.1.1", 4 | "description": "Logger for Elysia", 5 | "author": { 6 | "name": "tanishqmanuja", 7 | "url": "https://github.com/tanishqmanuja", 8 | "email": "tanishqmanuja@gmail.com" 9 | }, 10 | "type": "module", 11 | "main": "./dist/cjs/index.js", 12 | "module": "./dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "exports": { 15 | "./package.json": "./package.json", 16 | ".": { 17 | "types": "./dist/index.d.ts", 18 | "import": "./dist/index.js", 19 | "require": "./dist/cjs/index.js" 20 | } 21 | }, 22 | "files": [ 23 | "dist" 24 | ], 25 | "homepage": "https://github.com/tanishqmanuja/nice-logger", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/tanishqmanuja/nice-logger.git" 29 | }, 30 | "bugs": "https://github.com/tanishqmanuja/nice-logger/issues", 31 | "license": "MIT", 32 | "keywords": [ 33 | "elysia", 34 | "logger" 35 | ], 36 | "scripts": { 37 | "dev": "bun run --watch example/index.ts", 38 | "format": "prettier --write .", 39 | "format:check": "prettier --check .", 40 | "build": "bun run scripts/build.ts", 41 | "bump": "bun run scripts/bump.ts", 42 | "release": "bun run scripts/release.ts" 43 | }, 44 | "peerDependencies": { 45 | "elysia": ">=1.2.0" 46 | }, 47 | "devDependencies": { 48 | "@ianvs/prettier-plugin-sort-imports": "^4.4.0", 49 | "@types/bun": "^1.1.14", 50 | "elysia": "^1.2.9", 51 | "prettier": "^3.4.2", 52 | "tsup": "^8.3.5", 53 | "typescript": "^5.7.2" 54 | }, 55 | "dependencies": { 56 | "picocolors": "^1.1.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { $ } from "bun"; 2 | import pc from "picocolors"; 3 | import { build, type Options } from "tsup"; 4 | 5 | await $`rm -rf dist`; 6 | 7 | const tsupConfig: Options = { 8 | entry: ["src/**/*.ts"], 9 | splitting: false, 10 | sourcemap: false, 11 | clean: true, 12 | bundle: true, 13 | } satisfies Options; 14 | 15 | await Promise.all([ 16 | build({ 17 | outDir: "dist", 18 | format: "esm", 19 | target: "node20", 20 | cjsInterop: false, 21 | ...tsupConfig, 22 | }), 23 | build({ 24 | outDir: "dist/cjs", 25 | format: "cjs", 26 | target: "node20", 27 | ...tsupConfig, 28 | }), 29 | ]); 30 | 31 | await $`tsc --project tsconfig.dts.json`.then(() => 32 | console.log(pc.magenta("DTS"), "⚡ Files generated"), 33 | ); 34 | 35 | await Promise.all([$`cp dist/*.d.ts dist/cjs`]); 36 | 37 | process.exit(); 38 | -------------------------------------------------------------------------------- /scripts/bump.ts: -------------------------------------------------------------------------------- 1 | import { $ } from "bun"; 2 | 3 | const bump = process.argv[2]; 4 | 5 | if (!bump) { 6 | console.error("Missing bump"); 7 | process.exit(1); 8 | } 9 | 10 | if (!["patch", "minor", "major"].includes(bump)) { 11 | console.error("Invalid bump"); 12 | process.exit(1); 13 | } 14 | 15 | await $`npm version ${bump} -m "chore: bump version"`; 16 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | import { $ } from "bun"; 2 | 3 | import { version } from "../package.json"; 4 | 5 | const WORKFLOW_ID = "release.yaml"; 6 | 7 | const bump = process.argv[2] ?? "patch"; 8 | const dryRun = process.argv.includes("--dry-run"); 9 | 10 | if (!bump) { 11 | console.error("Missing bump"); 12 | process.exit(1); 13 | } 14 | 15 | if (!["patch", "minor", "major"].includes(bump)) { 16 | console.error("Invalid bump"); 17 | process.exit(1); 18 | } 19 | 20 | const { owner, repo } = await $`git config --get remote.origin.url` 21 | .quiet() 22 | .text() 23 | .then(url => { 24 | const [owner, repo] = url 25 | .trim() 26 | .replace(/^.*github\.com(\:|\/)?/, "") 27 | .replace(".git", "") 28 | .split("/"); 29 | 30 | if (!owner || !repo) { 31 | throw new Error("Cannot find owner or repo"); 32 | } 33 | return { owner, repo }; 34 | }); 35 | 36 | console.log(`Current Version: ${version}`); 37 | console.log(`Bump Type: ${bump}`); 38 | console.log(`Repo: ${owner}/${repo}`); 39 | 40 | if (dryRun) { 41 | process.exit(0); 42 | } 43 | 44 | console.log("\nDispatching workflow..."); 45 | 46 | await $`gh api --method POST -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \ 47 | /repos/${owner}/${repo}/actions/workflows/${WORKFLOW_ID}/dispatches -f "ref=main" -f "inputs[bump]=${bump}"` 48 | .quiet() 49 | .then(() => { 50 | console.log("Dispatched!"); 51 | process.exit(0); 52 | }) 53 | .catch(e => { 54 | console.log(e); 55 | process.exit(1); 56 | }); 57 | -------------------------------------------------------------------------------- /src/formatters.ts: -------------------------------------------------------------------------------- 1 | import pc from "picocolors"; 2 | import type { Formatter } from "picocolors/types"; 3 | 4 | /* Formatter for DURATION */ 5 | 6 | const UNITS = ["µs", "ms", "s"]; 7 | const DURATION_FORMATTER = Intl.NumberFormat(undefined, { 8 | maximumFractionDigits: 2, 9 | }); 10 | 11 | export function duration(duration: number | null): string { 12 | if (!duration) { 13 | return "-/-"; 14 | } 15 | let unitIndex = 0; 16 | while (duration >= 1000 && unitIndex < UNITS.length - 1) { 17 | duration /= 1000; 18 | unitIndex++; 19 | } 20 | 21 | return `${DURATION_FORMATTER.format(duration)}${UNITS[unitIndex]}`; 22 | } 23 | 24 | /* Formatter for METHOD */ 25 | 26 | const METHOD_COLOR_LUT = { 27 | GET: pc.green, 28 | POST: pc.blue, 29 | PUT: pc.yellow, 30 | DELETE: pc.red, 31 | PATCH: pc.magenta, 32 | OPTIONS: pc.cyan, 33 | HEAD: pc.gray, 34 | }; 35 | 36 | export function method(method: string): string { 37 | const colorer = (METHOD_COLOR_LUT as Record)[ 38 | method.toUpperCase() 39 | ]; 40 | 41 | if (colorer) { 42 | return colorer(method); 43 | } else { 44 | return method; 45 | } 46 | } 47 | 48 | /* Formatter for STATUS */ 49 | 50 | const STATUS_COLOR_LUT = { 51 | 200: pc.green, 52 | 201: pc.blue, 53 | 204: pc.yellow, 54 | 400: pc.red, 55 | 401: pc.magenta, 56 | 403: pc.cyan, 57 | 404: pc.gray, 58 | 500: pc.gray, 59 | }; 60 | 61 | export function status(status: string | number | undefined): string { 62 | if (status === undefined) { 63 | return ""; 64 | } 65 | 66 | const colorer = (STATUS_COLOR_LUT as Record)[+status]; 67 | 68 | if (colorer) { 69 | return colorer(status); 70 | } else { 71 | return status.toString(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from "elysia"; 2 | import pc from "picocolors"; 3 | 4 | import * as fmt from "./formatters"; 5 | 6 | const REQUEST_START_TIME_KEY = "@tqman/nice-logger/request-start-time"; 7 | 8 | export interface LoggerOptions { 9 | /** 10 | * Determine whether logging is enabled 11 | * 12 | * @default NODE_ENV !== 'production' 13 | */ 14 | enabled?: boolean; 15 | /** 16 | * Determines the mode of the logger 17 | * 18 | * - 'live' - logs every request and response 19 | * - 'combined' - logs request and response in a single line 20 | * 21 | * @default 'combined' 22 | */ 23 | mode?: "combined" | "live"; 24 | /** 25 | * Whether to print timestamp at the beginning of each line 26 | * could be a function that returns a string 27 | * 28 | * @default false 29 | */ 30 | withTimestamp?: boolean | (() => string); 31 | /** 32 | * Whether to print banner at the beginning of each line 33 | * 34 | * @default false 35 | */ 36 | withBanner?: 37 | | boolean 38 | | (() => void) 39 | | Record string | undefined)>; 40 | } 41 | 42 | /** 43 | * A Nice and Simple logger plugin for Elysia 44 | */ 45 | export const logger = (options: LoggerOptions = {}) => { 46 | const { enabled = process.env.NODE_ENV !== "production", mode = "combined" } = 47 | options; 48 | 49 | const app = new Elysia({ 50 | name: "@tqman/nice-logger", 51 | seed: options, 52 | }); 53 | 54 | if (!enabled) return app; 55 | 56 | const getTimestamp = 57 | typeof options.withTimestamp === "function" 58 | ? options.withTimestamp 59 | : () => new Date().toLocaleString(); 60 | 61 | app 62 | .onStart(ctx => { 63 | if (!options.withBanner) { 64 | return; 65 | } 66 | 67 | if (typeof options.withBanner === "function") { 68 | options.withBanner(); 69 | return; 70 | } 71 | 72 | const ELYSIA_VERSION = require("elysia/package.json").version; 73 | console.log(`🦊 ${pc.green(`${pc.bold("Elysia")} v${ELYSIA_VERSION}`)}`); 74 | 75 | if (typeof options.withBanner === "object") { 76 | Object.entries(options.withBanner).forEach(([key, value]) => { 77 | const v = typeof value === "function" ? value(ctx) : value; 78 | 79 | if (v) { 80 | console.log(`${pc.green(" ➜ ")} ${pc.bold(key)}: ${pc.cyan(v)}`); 81 | } 82 | }); 83 | 84 | // empty line 85 | console.log(); 86 | return; 87 | } 88 | 89 | console.log( 90 | `${pc.green(" ➜ ")} ${pc.bold("Server")}: ${pc.cyan(String(ctx.server?.url))}\n`, 91 | ); 92 | }) 93 | .onRequest(ctx => { 94 | ctx.store = { 95 | ...ctx.store, 96 | [REQUEST_START_TIME_KEY]: process.hrtime.bigint(), 97 | }; 98 | 99 | if (mode === "live") { 100 | const url = new URL(ctx.request.url); 101 | 102 | const components = [ 103 | options.withTimestamp ? pc.dim(`[${getTimestamp()}]`) : "", 104 | pc.blue("--->"), 105 | pc.bold(fmt.method(ctx.request.method)), 106 | url.pathname, 107 | ]; 108 | console.log(components.join(" ")); 109 | } 110 | }) 111 | .onAfterResponse(({ request, set, response, store }) => { 112 | if (response instanceof Error) { 113 | return; 114 | } 115 | 116 | const url = new URL(request.url); 117 | const duration = 118 | Number( 119 | process.hrtime.bigint() - (store as any)[REQUEST_START_TIME_KEY], 120 | ) / 1000; 121 | 122 | const sign = mode === "combined" ? pc.green("✓") : pc.green("<---"); 123 | const components = [ 124 | options.withTimestamp ? pc.dim(`[${getTimestamp()}]`) : "", 125 | sign, 126 | pc.bold(fmt.method(request.method)), 127 | url.pathname, 128 | fmt.status(set.status), 129 | pc.dim(`[${fmt.duration(duration)}]`), 130 | ]; 131 | console.log(components.join(" ")); 132 | }) 133 | .onError(({ request, error, store }) => { 134 | const url = new URL(request.url); 135 | const duration = (store as any)[REQUEST_START_TIME_KEY] 136 | ? Number( 137 | process.hrtime.bigint() - (store as any)[REQUEST_START_TIME_KEY], 138 | ) / 1000 139 | : null; 140 | const status = "status" in error ? error.status : 500; 141 | 142 | const sign = mode === "combined" ? pc.red("✗") : pc.red("<-x-"); 143 | const components = [ 144 | options.withTimestamp ? pc.dim(`[${getTimestamp()}]`) : "", 145 | sign, 146 | pc.bold(fmt.method(request.method)), 147 | url.pathname, 148 | fmt.status(status), 149 | pc.dim(`[${fmt.duration(duration)}]`), 150 | ]; 151 | console.log(components.join(" ")); 152 | }); 153 | 154 | return app.as("plugin"); 155 | }; 156 | -------------------------------------------------------------------------------- /tsconfig.dts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "allowSyntheticDefaultImports": true, 6 | 7 | "noEmit": false, 8 | "declaration": true, 9 | "emitDeclarationOnly": true, 10 | 11 | "rootDir": "./src", 12 | "outDir": "./dist", 13 | }, 14 | "include": [ 15 | "./src/**/*.ts", 16 | ] 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false, 26 | 27 | // Paths 28 | "baseUrl": ".", 29 | "paths": { 30 | "@tqman/nice-logger": ["src/index.ts"] 31 | } 32 | } 33 | } 34 | --------------------------------------------------------------------------------