├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src └── index.ts ├── tests └── server.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | node_modules 171 | 172 | #lock file 173 | package-lock.json 174 | pnpm-lock.yaml 175 | yaml.lock 176 | bun.lockb -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Tristan Isham 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Logysia 3 | A logging middleware for the [Elysia](https://elysiajs.com) web framework. Developed with [Bun](https://bun.sh). 4 | 5 | 6 | ## Installation 7 | 8 | ```sh 9 | bun add @grotto/logysia 10 | ``` 11 | ## Usage/Examples 12 | 13 | ```typescript 14 | import { logger } from '@grotto/logysia'; 15 | import { Elysia } from "elysia"; 16 | 17 | 18 | if (import.meta.main) { 19 | const app = new Elysia() 20 | // These are the default options. You do not need to copy this down 21 | .use(logger({ 22 | logIP: false, 23 | writer: { 24 | write(msg: string) { 25 | console.log(msg) 26 | } 27 | } 28 | })) 29 | .get("/", ctx => "Hello, world!") 30 | .listen(3000); 31 | } 32 | ``` 33 | 34 | ## Configuration 35 | 36 | | Option | Description | 37 | | :------: | :------------------------------------------------------------- | 38 | | `logIP` | Displays the incoming IP Address based on the XFF Header | 39 | | `writer` | Uses `write` function to send the log. Defaults to the console | 40 | 41 | ## Result 42 | ![Alt text](https://i.ibb.co/5YknHt6/image.png) 43 | 44 | Logysia also supports printing when there are errors in your application. 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@grotto/logysia", 3 | "version": "0.1.6", 4 | "type": "module", 5 | "description": "Logging middleware for Bun's Elysia web framework", 6 | "module": "./src/index.ts", 7 | "main": "./src/index.ts", 8 | "types": "./src/index.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./src/index.ts", 12 | "import": "./src/index.ts", 13 | "default": "./src/index.ts" 14 | } 15 | }, 16 | "files": [ 17 | "src", 18 | "README.md" 19 | ], 20 | "scripts": { 21 | "dev": "bun run --watch --hot tests/server.ts" 22 | }, 23 | "peerDependencies": { 24 | "elysia": "^1.1.25" 25 | }, 26 | "devDependencies": { 27 | "bun-types": "^1.1.36", 28 | "elysia": "^1.1.25", 29 | "typescript": "^5.7.2" 30 | }, 31 | "dependencies": { 32 | "picocolors": "^1.1.1" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/tristanisham/logysia.git" 37 | }, 38 | "keywords": [ 39 | "logging", 40 | "elysia", 41 | "elysiajs", 42 | "web", 43 | "logger", 44 | "middleware", 45 | "bun" 46 | ], 47 | "author": "Tristan Isham", 48 | "license": "ISC", 49 | "bugs": { 50 | "url": "https://github.com/tristanisham/logysia/issues" 51 | }, 52 | "homepage": "https://github.com/tristanisham/logysia#readme", 53 | "engines": { 54 | "bun": ">=1.0.0" 55 | } 56 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Elysia from "elysia" 2 | import * as pc from "picocolors" 3 | import process from "process" 4 | 5 | interface Writer { 6 | write: (message: string) => void 7 | } 8 | 9 | const consoleWriter: Writer = { 10 | write(message: string) { 11 | console.log(message) 12 | } 13 | } 14 | 15 | interface Options { 16 | logIP?: boolean, 17 | writer?: Writer 18 | } 19 | 20 | 21 | export const logger = (options?: Options) => { 22 | const { write } = options?.writer || consoleWriter 23 | return new Elysia({ 24 | name: "@grotto/logysia" 25 | }) 26 | .onRequest((ctx) => { 27 | ctx.store = { ...ctx.store, beforeTime: process.hrtime.bigint() } 28 | }) 29 | 30 | .onBeforeHandle({ as: "global" }, (ctx) => { 31 | ctx.store = { ...ctx.store, beforeTime: process.hrtime.bigint() } 32 | }) 33 | .onAfterHandle({ as: "global" }, ({ request, store }) => { 34 | const logStr: string[] = [] 35 | if (options !== undefined && options.logIP) { 36 | if (request.headers.get("X-Forwarded-For")) { 37 | logStr.push(`[${pc.cyan(request.headers.get("X-Forwarded-For"))}]`) 38 | } 39 | } 40 | 41 | logStr.push(methodString(request.method)) 42 | 43 | logStr.push(new URL(request.url).pathname) 44 | const beforeTime: bigint = (store as any).beforeTime; 45 | 46 | logStr.push(durationString(beforeTime)) 47 | 48 | write(logStr.join(" ")) 49 | }) 50 | .onError({ as: "global" }, ({ request, error, store }) => { 51 | const logStr: string[] = [] 52 | 53 | logStr.push(pc.red(methodString(request.method))) 54 | 55 | logStr.push(new URL(request.url).pathname) 56 | 57 | logStr.push(pc.red("Error")) 58 | 59 | if ("status" in error) { 60 | logStr.push(String(error.status)) 61 | } 62 | 63 | logStr.push(error.message) 64 | const beforeTime: bigint = (store as any).beforeTime; 65 | 66 | logStr.push(durationString(beforeTime)) 67 | 68 | write(logStr.join(" ")) 69 | }) 70 | } 71 | 72 | 73 | function durationString(beforeTime: bigint): string { 74 | const now = process.hrtime.bigint(); 75 | const timeDifference = now - beforeTime; 76 | const nanoseconds = Number(timeDifference); 77 | 78 | const durationInMicroseconds = (nanoseconds / 1e3).toFixed(0); // Convert to microseconds 79 | const durationInMilliseconds = (nanoseconds / 1e6).toFixed(0); // Convert to milliseconds 80 | let timeMessage: string = ""; 81 | 82 | if (nanoseconds >= 1e9) { 83 | const seconds = (nanoseconds / 1e9).toFixed(2); 84 | timeMessage = `| ${seconds}s`; 85 | } else if (nanoseconds >= 1e6) { 86 | timeMessage = `| ${durationInMilliseconds}ms`; 87 | } else if (nanoseconds >= 1e3) { 88 | timeMessage = `| ${durationInMicroseconds}µs`; 89 | } else { 90 | timeMessage = `| ${nanoseconds}ns`; 91 | } 92 | 93 | return timeMessage; 94 | } 95 | 96 | 97 | function methodString(method: string): string { 98 | switch (method) { 99 | case "GET": 100 | // Handle GET request 101 | return pc.white("GET") 102 | 103 | case "POST": 104 | // Handle POST request 105 | return pc.yellow("POST") 106 | 107 | case "PUT": 108 | // Handle PUT request 109 | return pc.blue("PUT") 110 | 111 | case "DELETE": 112 | // Handle DELETE request 113 | return pc.red("DELETE") 114 | 115 | case "PATCH": 116 | // Handle PATCH request 117 | return pc.green("PATCH") 118 | 119 | case "OPTIONS": 120 | // Handle OPTIONS request 121 | return pc.gray("OPTIONS") 122 | 123 | case "HEAD": 124 | // Handle HEAD request 125 | return pc.magenta("HEAD") 126 | 127 | default: 128 | // Handle unknown request method 129 | return method 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/server.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../src/index.ts"; 2 | import { Elysia } from "elysia"; 3 | 4 | 5 | if (import.meta.main) { 6 | const PORT = process.env["PORT"] || 3000 7 | const app = new Elysia() 8 | // These are the default options. You do not need to copy this down 9 | .use(logger({ 10 | logIP: true, 11 | writer: { 12 | write(msg: string) { 13 | console.log(msg) 14 | } 15 | } 16 | })) 17 | .get("/", ctx => "Hello, world!") 18 | .listen(PORT, () => console.log(`http://localhost:${PORT}`)); 19 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "types": ["bun-types"], 8 | "moduleDetection": "force", 9 | "jsx": "react-jsx", 10 | "allowJs": true, 11 | 12 | // Bundler mode 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "verbatimModuleSyntax": true, 16 | "noEmit": true, 17 | 18 | // Best practices 19 | "strict": true, 20 | "skipLibCheck": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | // Some stricter flags 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noPropertyAccessFromIndexSignature": true 27 | } 28 | } --------------------------------------------------------------------------------