@elysiumjs/hermes 0.4.0 → 0.5.0

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,77 @@
1
1
  # @elysiumjs/hermes
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#11](https://github.com/na2axl/elysiumjs/pull/11) [`632d77b`](https://github.com/na2axl/elysiumjs/commit/632d77b561fc43ade605fb69b06ac1b9b9a2ffc4) Thanks [@na2axl](https://github.com/na2axl)! - Services are identified by their class, or by the name given to `@Service.register({ name })`. Class names are never used as identifiers, so services resolve the same way in minified builds; `bun run check:minified` in core bundles a minified app and checks that every service resolves. `@Service.inject()` without an argument injects the class of the parameter type, and throws when the type is unknown. Factory services in a module's `services` take a class or a name in `name`.
8
+
9
+ `ServiceScope.REQUEST` gives one instance per HTTP request, shared by the request's middlewares, controller and handler. `Service.runInRequestScope(fn)` opens a request scope outside a route. Resolving a request-scoped service outside a request throws `OutsideRequestScopeError`, and a singleton (or a server-scoped controller) depending on one throws `ScopeMismatchError`.
10
+
11
+ `HermesModule` registers the logger by the `HermesLogger` class and as `logger`.
12
+
13
+ - [#16](https://github.com/na2axl/elysiumjs/pull/16) [`ac96e55`](https://github.com/na2axl/elysiumjs/commit/ac96e55874b0ffe350b287c4278cd85afecb7576) Thanks [@na2axl](https://github.com/na2axl)! - Redis, databases, logs, jobs and builds on Bun, Node.js and Cloudflare Workers.
14
+
15
+ Breaking:
16
+
17
+ - The level shortcuts of `InteractsWithConsole` (`info`, `success`, `warning`, `error` and `debug`) write a tag (`[I]`, `[S]`, `[W]`, `[E]` and `[D]`, in color) before the message, which `InteractsWithConsole.useTags()` changes. `trace()` writes to stderr.
18
+ - Outside Bun, a Drizzle connection needs a `driver`. Without one, the `connection` and `client` props are a Bun SQL connection, which is only available on Bun; elsewhere the connection throws `MissingDriverError`, which names the drivers.
19
+
20
+ Added:
21
+
22
+ - Redis adapters (`@elysiumjs/redis`): `ioredis(create)` plugs an ioredis client into the Redis port on Node.js, and `upstash({ token, url?, fetch? })` talks to Upstash over its REST API, for Workers (no pub/sub). `bunRedis` explains which adapter to use when it runs outside Bun.
23
+ - Drizzle drivers (`@elysiumjs/mnemosyne-drizzle`): a `DrizzleDriver` port set with the `driver` prop of a connection, and drivers on subpaths so that only the chosen database client is imported: `/bun-sql` (`bunSql()`), `/postgres-js` (`postgresJs()`), `/node-postgres` (`nodePostgres()`) and `/hyperdrive` (`hyperdrive({ binding, driver })`, a client per Worker invocation over a Cloudflare Hyperdrive binding). `DrizzleModule` closes connections through their driver, and `migration:run` runs on any driver.
24
+ - `InteractsWithConsole.write()` and `Runtime.write()` take the level of the line (`OutputLevel`), which the level shortcuts pass. On Cloudflare Workers, a line is written with the console method of its level, so Workers Logs keep it.
25
+ - `JsonConsoleLogger` (`@elysiumjs/hermes`): JSON lines through the console of the runtime (warnings and errors on stderr), with levels, error serialization and `child()`. `HermesModule.with({ transport: 'console' | 'pino' })` picks it, and it is the default on Workers. `RequestLogInterceptor` writes one structured line per request: method, route, status, duration, client address and request id.
26
+ - Scheduled jobs (`@elysiumjs/heracles`): `@Schedule(cron, { timezone, args })` on a job class, a five-field cron parser (`Cron`, with lists, ranges, steps and names), and a `Scheduler` that dispatches the jobs on time on Bun and Node.js, taking a Redis lock per tick when a Redis connection is set. The `heracles:schedule` command runs it.
27
+ - `@elysiumjs/heracles-workers`: `CloudflareQueueTransport` sends jobs to a Cloudflare Queue (dispatch delays become `delaySeconds`), and `HeraclesWorkersModule` consumes them in the Worker's `queue` handler (ack on success, retry with a delay up to the job's retries) and dispatches the `@Schedule()` jobs whose expression is the cron of a trigger.
28
+ - Queue handlers (`@elysiumjs/runtime-workers`) can settle a message themselves with `ack()` or `retry({ delaySeconds })`; the messages they leave are acked when no handler failed on them.
29
+ - `styx build --target node` (`@elysiumjs/hephaestus`) bundles the app into one ES module for Node.js, with decorator metadata emitted at build time, run with `node dist/server.js serve`. `styx build --target workers` runs from the CLI, resolves packages to their browser entry, and leaves Node.js modules imported by bare names to workerd (`nodejs_compat`).
30
+ - `styx create` (`@elysiumjs/styx`) takes `--runtime bun|node|workers`, `--database`, `--cache` and `--package-manager bun|npm|pnpm`. Each runtime gets its entry, tsconfig types, scripts and drivers: postgres.js and ioredis on Node.js; Hyperdrive, Upstash, console logs and a `wrangler.jsonc` on Workers.
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [[`6bc48fe`](https://github.com/na2axl/elysiumjs/commit/6bc48febd51f7090c582e73f349eaeff7261241c), [`e9c3d98`](https://github.com/na2axl/elysiumjs/commit/e9c3d98dd2714b69100215f7c8a80046da463148), [`557a3ca`](https://github.com/na2axl/elysiumjs/commit/557a3cab7af8053a8bab9944e492e5022d91aa82), [`ccc3f5b`](https://github.com/na2axl/elysiumjs/commit/ccc3f5bb904d1bf52d8c72ebf7ea8d10f617a8c4), [`6789879`](https://github.com/na2axl/elysiumjs/commit/678987976ecfd46b534b65276e0de3f448977562), [`87299fd`](https://github.com/na2axl/elysiumjs/commit/87299fd9da82d1a408ced1891c0c3d42991444e5), [`bee66bd`](https://github.com/na2axl/elysiumjs/commit/bee66bdc874b8f23fea356f7d3adf7555a48ca64), [`0057b67`](https://github.com/na2axl/elysiumjs/commit/0057b67df0e3abecab8a9accff6b55a6a64c21df), [`632d77b`](https://github.com/na2axl/elysiumjs/commit/632d77b561fc43ade605fb69b06ac1b9b9a2ffc4), [`1ea417e`](https://github.com/na2axl/elysiumjs/commit/1ea417e3583250bc2f975d795f398b7ff736115d), [`0956186`](https://github.com/na2axl/elysiumjs/commit/0956186db0ca5a83c0a6636aeb2a4c81a0033de4), [`fbcc7da`](https://github.com/na2axl/elysiumjs/commit/fbcc7dad127db29da42e7a4a5c2124966731cd88), [`ac96e55`](https://github.com/na2axl/elysiumjs/commit/ac96e55874b0ffe350b287c4278cd85afecb7576), [`98a16aa`](https://github.com/na2axl/elysiumjs/commit/98a16aae889d46f7a9fb5573188f4cbe3629dd3b)]:
35
+ - @elysiumjs/core@0.11.0
36
+
37
+ ## 0.4.3
38
+
39
+ ### Patch Changes
40
+
41
+ - [`04773f2`](https://github.com/na2axl/elysiumjs/commit/04773f2acd4ab1c762a80ee903715d90f1c08e21) Thanks [@na2axl](https://github.com/na2axl)! - Ship the type declarations. The packages pointed `types` at `dist/index.d.ts` but their build never wrote it, so TypeScript projects got no types for them.
42
+
43
+ ## 0.4.2
44
+
45
+ ### Patch Changes
46
+
47
+ - [`68a0cdd`](https://github.com/na2axl/elysiumjs/commit/68a0cdd8b2e4a003d6e0af2d78985623cae40428) Thanks [@na2axl](https://github.com/na2axl)! - Route the framework's own diagnostics through the logger service.
48
+
49
+ Breaking changes:
50
+
51
+ - `@Event.listen()` with a mode other than `on` or `once` now throws an `ElysiumError` when the class is defined, instead of logging to the console and ignoring the listener.
52
+
53
+ Fixes and additions:
54
+
55
+ - `getLogger(context?)` and `LOGGER_SERVICE` (`'logger'`) are exported by `@elysiumjs/core`. `getLogger()` returns the `logger` service when one is registered (such as the one `HermesModule` provides), and a `ConsoleLogger` otherwise or when the service cannot be built or fails to create a child logger. It is resolved at call time from the global scope only, so a module's own `logger` service does not replace it, and returns a child logger bound to `context` when one is given.
56
+ - `Middleware.logger` (used by `RequestLoggerMiddleware`) falls back to a `ConsoleLogger` when no `logger` service is registered. It threw before.
57
+ - Core diagnostics go through the logger with structured context instead of `console.*`: the debug-mode request log (`debug`, with `method` and `url`), `onDestroy` failures on shutdown (`error`, with `target` and `error`), WAMP connection attempts (`warn` while retrying, `error` when giving up, with `connection`, `attempt`/`attempts`, `maxRetries` and `error`), and WAMP registrations and subscriptions (`info`, with `connection`, `topic` and the registration or subscription id).
58
+ - In debug mode, incoming requests are logged through the logger at `debug` level instead of printed with `console.log`. With `HermesModule`, they show only when its `level` is `debug` or `trace`.
59
+ - A failed WAMP registration or subscription is logged at `error` level and emitted as `elysium:error` with an error naming the topic and the connection, instead of becoming an unhandled rejection.
60
+ - The `elysium:error` emitted for a WAMP client error names the connection and its URL and includes the client's message (or the value it reported, when it is not an `Error`), with that value as `cause` when there is one.
61
+ - `HermesLogger` serialises an `error` field of the log context like pino's `err` (type, message and stack), so logged errors keep their stack trace. Elysium logs errors under `error`.
62
+ - `@Logged` resolves its fallback logger with `getLogger()`, and logs a thrown error whole under `error` (it logged only its message), so its stack trace is kept.
63
+ - The `elysium:error` emitted when a WAMP connection gives up states the number of attempts actually made (the first one plus the retries), as the logged `attempts` does. It stated `maxRetries` before.
64
+
65
+ - Updated dependencies [[`68a0cdd`](https://github.com/na2axl/elysiumjs/commit/68a0cdd8b2e4a003d6e0af2d78985623cae40428), [`2c49acd`](https://github.com/na2axl/elysiumjs/commit/2c49acd3b552aa43765d70339fa216961499e1b5), [`76ba0ff`](https://github.com/na2axl/elysiumjs/commit/76ba0ff18f3c196cebc2189426328f67cb699f46), [`4ebf029`](https://github.com/na2axl/elysiumjs/commit/4ebf029611e6d753a4f1bfdbe247a75d954d9ded), [`bacce71`](https://github.com/na2axl/elysiumjs/commit/bacce7146f667a1a46bf474dc0344fb0111ae12a)]:
66
+ - @elysiumjs/core@0.10.0
67
+
68
+ ## 0.4.1
69
+
70
+ ### Patch Changes
71
+
72
+ - Updated dependencies [[`5fef4a7`](https://github.com/na2axl/elysiumjs/commit/5fef4a7199f502fc4fe035299e7670739459a108)]:
73
+ - @elysiumjs/core@0.9.0
74
+
3
75
  ## 0.4.0
4
76
 
5
77
  ### Minor Changes
package/README.md CHANGED
@@ -10,7 +10,7 @@ bun add @elysiumjs/hermes
10
10
 
11
11
  ## Usage
12
12
 
13
- Add `HermesModule` to your application. It provides the logger as the global services `HermesLogger` and `logger`.
13
+ Add `HermesModule` to your application. It provides the logger as global services, by the `HermesLogger` class and by the name `logger`.
14
14
 
15
15
  ```ts
16
16
  import { Application } from '@elysiumjs/core';
@@ -0,0 +1,16 @@
1
+ import type { LogLevel } from '../types';
2
+ export interface LoggedOptions {
3
+ level?: LogLevel;
4
+ includeArgs?: boolean;
5
+ includeResult?: boolean;
6
+ includeTiming?: boolean;
7
+ }
8
+ /**
9
+ * Decorator that logs method entry/exit with optional timing.
10
+ *
11
+ * Logger is resolved at CALL TIME (not decorator definition time)
12
+ * to avoid capturing a stale logger reference.
13
+ *
14
+ * @author Axel Nana <axel.nana@workbud.com>
15
+ */
16
+ export declare function Logged(options?: LoggedOptions): MethodDecorator;
@@ -0,0 +1,25 @@
1
+ import type { HermesConfig } from './types';
2
+ declare const HermesModule_base: (abstract new () => {
3
+ readonly config: HermesConfig;
4
+ onInit(): Promise<void> | void;
5
+ onDestroy(): Promise<void> | void;
6
+ onServe(_app: import("elysia").Elysia<any>): Promise<void> | void;
7
+ }) & {
8
+ readonly config: string;
9
+ with(config: Partial<HermesConfig> | (() => Partial<HermesConfig> | Promise<Partial<HermesConfig>>)): import("@elysiumjs/core").ConfiguredModule<HermesConfig>;
10
+ resolveConfig(input?: Partial<HermesConfig> | undefined): HermesConfig;
11
+ register<TConfig_1 = any>(props?: import("@elysiumjs/core").ModuleProps<TConfig_1>): (target: import("@elysiumjs/core").ModuleClass) => void;
12
+ configurable<TConfig extends object>(options?: {
13
+ defaults?: Partial<TConfig>;
14
+ required?: (keyof TConfig)[];
15
+ }): any;
16
+ };
17
+ /**
18
+ * Provides the {@link HermesLogger} as global services, by its class and as `logger`
19
+ * (the same instance), built from the module config. With `transport: 'console'` (the default
20
+ * on Cloudflare Workers), `logger` is a {@link JsonConsoleLogger} instead.
21
+ * @author Axel Nana <axel.nana@workbud.com>
22
+ */
23
+ export declare class HermesModule extends HermesModule_base {
24
+ }
25
+ export {};
@@ -0,0 +1,9 @@
1
+ export { HermesModule } from './hermes.module';
2
+ export { RequestLogInterceptor } from './interceptors/request-log.interceptor';
3
+ export { JsonConsoleLogger } from './services/json-console.logger';
4
+ export { HermesLogger } from './services/logger.service';
5
+ export { HttpTransport } from './transports/http.transport';
6
+ export type { HttpTransportOptions } from './transports/http.transport';
7
+ export { Logged } from './decorators/logged';
8
+ export type { LoggedOptions } from './decorators/logged';
9
+ export type { HermesConfig, LogLevel, LogFormat, TransportConfig } from './types';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- var __legacyDecorateClassTS=function(decorators,target,key,desc){var c=arguments.length,r=c<3?target:desc===null?desc=Object.getOwnPropertyDescriptor(target,key):desc,d;if(typeof Reflect==="object"&&typeof Reflect.decorate==="function")r=Reflect.decorate(decorators,target,key,desc);else for(var i=decorators.length-1;i>=0;i--)if(d=decorators[i])r=(c<3?d(r):c>3?d(target,key,r):d(target,key))||r;return c>3&&r&&Object.defineProperty(target,key,r),r};var __require=import.meta.require;import{Module}from"@elysiumjs/core";import{Service}from"@elysiumjs/core";import pino from"pino";var DEFAULT_CONFIG={level:"info",format:"json"};class HermesLogger{pino;config;static make(name){return Service.get(HermesLogger)?.child({name})??new HermesLogger({name})}constructor(config={}){this.config={...DEFAULT_CONFIG,...config},this.pino=this.createPinoInstance()}createPinoInstance(){let options={level:this.config.level,name:this.config.name};if(this.config.format==="pretty")return pino(options,pino.transport({target:"pino-pretty"}));return pino(options)}trace(msg,ctx){this.pino.trace(ctx??{},msg)}debug(msg,ctx){this.pino.debug(ctx??{},msg)}info(msg,ctx){this.pino.info(ctx??{},msg)}warn(msg,ctx){this.pino.warn(ctx??{},msg)}error(msg,ctx){this.pino.error(ctx??{},msg)}fatal(msg,ctx){this.pino.fatal(ctx??{},msg)}flush(){this.pino.flush?.()}onDestroy(){this.flush()}child(ctx){let child=Object.create(this);return child.pino=this.pino.child(ctx),child}}class HermesModule extends Module.configurable({defaults:{...DEFAULT_CONFIG}}){}HermesModule=__legacyDecorateClassTS([Module.register({services:(config)=>[{name:HermesLogger.name,global:!0,factory:()=>new HermesLogger(config)},{name:"logger",global:!0,factory:(_config,get)=>get(HermesLogger.name)}]})],HermesModule);class HttpTransport{options;buffer=[];flushTimer;maxBufferSize;constructor(options){this.options=options;this.options.batchSize??=100,this.options.flushInterval??=5000,this.maxBufferSize=this.options.maxBufferSize??1e4,this.flushTimer=setInterval(()=>this.flush(),this.options.flushInterval)}write(formatted){if(this.buffer.length>=this.maxBufferSize)this.buffer.shift();if(this.buffer.push(formatted),this.buffer.length>=this.options.batchSize)this.flush()}async flush(){if(this.buffer.length===0)return;let batch=this.buffer.splice(0);try{await fetch(this.options.url,{method:"POST",headers:{"Content-Type":"application/json",...this.options.headers},body:JSON.stringify({logs:batch})})}catch{let space=this.maxBufferSize-this.buffer.length;if(space>0)this.buffer.unshift(...batch.slice(0,space))}}async close(){if(this.flushTimer)clearInterval(this.flushTimer);await this.flush()}}import{ConsoleLogger}from"@elysiumjs/core";function Logged(options={}){return function(target,propertyKey,descriptor){let original=descriptor.value,methodName=`${target.constructor.name}.${String(propertyKey)}`;return descriptor.value=async function(...args){let logger=this.logger??getServiceLogger(),startTime=Date.now();if(options.includeArgs)logger.debug(`[${methodName}] Entry`,{args});try{let result=await original.apply(this,args);if(options.includeResult||options.includeTiming){let duration=Date.now()-startTime;logger[options.level??"debug"](`[${methodName}] Exit`,{...options.includeResult&&{result},...options.includeTiming&&{duration:`${duration}ms`}})}return result}catch(error){let duration=Date.now()-startTime;throw logger.error(`[${methodName}] Error`,{error:error instanceof Error?error.message:error,duration:`${duration}ms`}),error}},descriptor}}function getServiceLogger(){try{let{Service}=__require("@elysiumjs/core");return Service.get("logger")??new ConsoleLogger}catch{return new ConsoleLogger}}export{HermesLogger,HermesModule,HttpTransport,Logged};
2
+ var __legacyDecorateClassTS=function(decorators,target,key,desc){var c=arguments.length,r=c<3?target:desc===null?desc=Object.getOwnPropertyDescriptor(target,key):desc,d;if(typeof Reflect==="object"&&typeof Reflect.decorate==="function")r=Reflect.decorate(decorators,target,key,desc);else for(var i=decorators.length-1;i>=0;i--)if(d=decorators[i])r=(c<3?d(r):c>3?d(target,key,r):d(target,key))||r;return c>3&&r&&Object.defineProperty(target,key,r),r};import{LOGGER_SERVICE,Module}from"@elysiumjs/core";import{InteractsWithConsole}from"@elysiumjs/core";var DEFAULT_CONFIG={level:"info",format:"json"};var LEVELS={trace:10,debug:20,info:30,warn:40,error:50,fatal:60},serializable=(value)=>{if(value instanceof Error)return{type:value.name,message:value.message,stack:value.stack,...value.cause!==void 0?{cause:serializable(value.cause)}:{}};return value};class JsonConsoleLogger{#console=new InteractsWithConsole;#threshold;#bindings;constructor(config={},bindings={}){let level=config.level??DEFAULT_CONFIG.level;this.#threshold=LEVELS[level],this.#bindings={...config.name?{name:config.name}:{},...bindings},this.level=level}level;trace(msg,ctx){this.#write("trace",msg,ctx)}debug(msg,ctx){this.#write("debug",msg,ctx)}info(msg,ctx){this.#write("info",msg,ctx)}warn(msg,ctx){this.#write("warn",msg,ctx)}error(msg,ctx){this.#write("error",msg,ctx)}fatal(msg,ctx){this.#write("fatal",msg,ctx)}child(ctx){return new JsonConsoleLogger({level:this.level},{...this.#bindings,...ctx})}#write(level,msg,ctx){if(LEVELS[level]<this.#threshold)return;let entry={level:LEVELS[level],time:Date.now(),...this.#bindings};for(let[key,value]of Object.entries(ctx??{}))entry[key]=serializable(value);entry.msg=msg;let line;try{line=JSON.stringify(entry)}catch{line=JSON.stringify({level:entry.level,time:entry.time,msg,unserializable:!0})}this.#console.write(line,!0,LEVELS[level]>=LEVELS.warn?"stderr":"stdout",level)}}import{Service}from"@elysiumjs/core";import pino from"pino";class HermesLogger{pino;config;static make(name){return Service.get(HermesLogger)?.child({name})??new HermesLogger({name})}constructor(config={}){this.config={...DEFAULT_CONFIG,...config},this.pino=this.createPinoInstance()}createPinoInstance(){let options={level:this.config.level,name:this.config.name,serializers:{error:pino.stdSerializers.err}};if(this.config.format==="pretty")return pino(options,pino.transport({target:"pino-pretty"}));return pino(options)}trace(msg,ctx){this.pino.trace(ctx??{},msg)}debug(msg,ctx){this.pino.debug(ctx??{},msg)}info(msg,ctx){this.pino.info(ctx??{},msg)}warn(msg,ctx){this.pino.warn(ctx??{},msg)}error(msg,ctx){this.pino.error(ctx??{},msg)}fatal(msg,ctx){this.pino.fatal(ctx??{},msg)}flush(){this.pino.flush?.()}onDestroy(){this.flush()}child(ctx){let child=Object.create(this);return child.pino=this.pino.child(ctx),child}}var defaultTransport=()=>globalThis.navigator?.userAgent==="Cloudflare-Workers"?"console":"pino";class HermesModule extends Module.configurable({defaults:{...DEFAULT_CONFIG}}){}HermesModule=__legacyDecorateClassTS([Module.register({services:(config)=>[{name:HermesLogger,global:!0,factory:()=>new HermesLogger(config)},{name:LOGGER_SERVICE,global:!0,factory:(_config,get)=>(config.transport??defaultTransport())==="console"?new JsonConsoleLogger(config):get(HermesLogger)}]})],HermesModule);import{getLogger}from"@elysiumjs/core";var statusOf=(error)=>{let status=error?.status;return typeof status==="number"?status:500};class RequestLogInterceptor{async intercept(ctx,next){let started=performance.now(),log=(status,error)=>{let fields={method:ctx.request.method,path:new URL(ctx.request.url).pathname,status,durationMs:Math.round((performance.now()-started)*100)/100};if(typeof ctx.route==="string")fields.route=ctx.route;if(typeof ctx.ip==="string")fields.ip=ctx.ip;let requestId=ctx.request.headers.get("x-request-id");if(requestId)fields.requestId=requestId;if(error!==void 0&&status>=500)fields.error=error;let message=`${fields.method} ${fields.path} ${status}`,logger=getLogger();if(status>=500)logger.error(message,fields);else if(status>=400)logger.warn(message,fields);else logger.info(message,fields)};try{let result=await next(),status=result instanceof Response?result.status:Number(ctx.set.status??200);return log(Number.isFinite(status)?status:200),result}catch(error){throw log(statusOf(error),error),error}}}class HttpTransport{options;buffer=[];flushTimer;maxBufferSize;constructor(options){this.options=options;this.options.batchSize??=100,this.options.flushInterval??=5000,this.maxBufferSize=this.options.maxBufferSize??1e4,this.flushTimer=setInterval(()=>this.flush(),this.options.flushInterval)}write(formatted){if(this.buffer.length>=this.maxBufferSize)this.buffer.shift();if(this.buffer.push(formatted),this.buffer.length>=this.options.batchSize)this.flush()}async flush(){if(this.buffer.length===0)return;let batch=this.buffer.splice(0);try{await fetch(this.options.url,{method:"POST",headers:{"Content-Type":"application/json",...this.options.headers},body:JSON.stringify({logs:batch})})}catch{let space=this.maxBufferSize-this.buffer.length;if(space>0)this.buffer.unshift(...batch.slice(0,space))}}async close(){if(this.flushTimer)clearInterval(this.flushTimer);await this.flush()}}import{getLogger as getLogger2}from"@elysiumjs/core";function Logged(options={}){return function(target,propertyKey,descriptor){let original=descriptor.value,methodName=`${target.constructor.name}.${String(propertyKey)}`;return descriptor.value=async function(...args){let logger=this.logger??getLogger2(),startTime=Date.now();if(options.includeArgs)logger.debug(`[${methodName}] Entry`,{args});try{let result=await original.apply(this,args);if(options.includeResult||options.includeTiming){let duration=Date.now()-startTime;logger[options.level??"debug"](`[${methodName}] Exit`,{...options.includeResult&&{result},...options.includeTiming&&{duration:`${duration}ms`}})}return result}catch(error){let duration=Date.now()-startTime;throw logger.error(`[${methodName}] Error`,{error,duration:`${duration}ms`}),error}},descriptor}}export{HermesLogger,HermesModule,HttpTransport,JsonConsoleLogger,Logged,RequestLogInterceptor};
@@ -0,0 +1,16 @@
1
+ import type { Interceptor, InterceptorContext } from '@elysiumjs/core';
2
+ /**
3
+ * Writes one structured line per request through the app logger: the method, the path, the route,
4
+ * the status, the duration in milliseconds, the client address and the `x-request-id` header when
5
+ * present. Server errors are logged at `error`, client errors at `warn`, the rest at `info`. Use it
6
+ * as an app interceptor; streaming routes do not run interceptors.
7
+ * @author Axel Nana <axel.nana@workbud.com>
8
+ * @example
9
+ * ```ts
10
+ * @Application.register({ modules: [HermesModule], interceptors: [RequestLogInterceptor] })
11
+ * class App extends Application {}
12
+ * ```
13
+ */
14
+ export declare class RequestLogInterceptor implements Interceptor {
15
+ intercept(ctx: InterceptorContext, next: () => Promise<unknown>): Promise<unknown>;
16
+ }
@@ -0,0 +1,22 @@
1
+ import type { LoggerInterface } from '@elysiumjs/core';
2
+ import type { HermesConfig, LogLevel } from '../types';
3
+ /**
4
+ * A logger that writes one JSON object per line, with the same fields as the pino logger
5
+ * (`level`, `time`, `name`, `msg` and the context), through the console of the runtime: the
6
+ * standard streams on Bun and Node.js (warnings and errors on stderr), and the console method of
7
+ * each level on Cloudflare Workers, where Workers Logs index the fields and keep the level.
8
+ * @author Axel Nana <axel.nana@workbud.com>
9
+ */
10
+ export declare class JsonConsoleLogger implements LoggerInterface {
11
+ #private;
12
+ constructor(config?: Partial<HermesConfig>, bindings?: Record<string, unknown>);
13
+ /** The lowest level written. */
14
+ readonly level: LogLevel;
15
+ trace(msg: string, ctx?: Record<string, unknown>): void;
16
+ debug(msg: string, ctx?: Record<string, unknown>): void;
17
+ info(msg: string, ctx?: Record<string, unknown>): void;
18
+ warn(msg: string, ctx?: Record<string, unknown>): void;
19
+ error(msg: string, ctx?: Record<string, unknown>): void;
20
+ fatal(msg: string, ctx?: Record<string, unknown>): void;
21
+ child(ctx: Record<string, unknown>): JsonConsoleLogger;
22
+ }
@@ -0,0 +1,29 @@
1
+ import type { LoggerInterface } from '@elysiumjs/core';
2
+ import type { HermesConfig } from '../types';
3
+ /**
4
+ * Production-ready logger implementation wrapping Pino.
5
+ * Implements `LoggerInterface` from `@elysiumjs/core`.
6
+ *
7
+ * @author Axel Nana <axel.nana@workbud.com>
8
+ */
9
+ export declare class HermesLogger implements LoggerInterface {
10
+ private pino;
11
+ private config;
12
+ static make(name: string): HermesLogger;
13
+ constructor(config?: Partial<HermesConfig>);
14
+ private createPinoInstance;
15
+ trace(msg: string, ctx?: Record<string, unknown>): void;
16
+ debug(msg: string, ctx?: Record<string, unknown>): void;
17
+ info(msg: string, ctx?: Record<string, unknown>): void;
18
+ warn(msg: string, ctx?: Record<string, unknown>): void;
19
+ error(msg: string, ctx?: Record<string, unknown>): void;
20
+ fatal(msg: string, ctx?: Record<string, unknown>): void;
21
+ /**
22
+ * Flushes buffered log lines of the underlying transport, when it supports it.
23
+ * @author Axel Nana <axel.nana@workbud.com>
24
+ */
25
+ flush(): void;
26
+ /** Flushes on shutdown. Only called for a logger the container built. */
27
+ onDestroy(): void;
28
+ child(ctx: Record<string, unknown>): HermesLogger;
29
+ }
@@ -0,0 +1,22 @@
1
+ export interface HttpTransportOptions {
2
+ url: string;
3
+ headers?: Record<string, string>;
4
+ batchSize?: number;
5
+ flushInterval?: number;
6
+ maxBufferSize?: number;
7
+ }
8
+ /**
9
+ * HTTP transport for batching and sending logs to a remote endpoint.
10
+ *
11
+ * @author Axel Nana <axel.nana@workbud.com>
12
+ */
13
+ export declare class HttpTransport {
14
+ private options;
15
+ private buffer;
16
+ private flushTimer?;
17
+ private maxBufferSize;
18
+ constructor(options: HttpTransportOptions);
19
+ write(formatted: string): void;
20
+ flush(): Promise<void>;
21
+ close(): Promise<void>;
22
+ }
@@ -0,0 +1,18 @@
1
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
2
+ export type LogFormat = 'json' | 'pretty' | 'logfmt';
3
+ export interface TransportConfig {
4
+ type: 'console' | 'file' | 'http';
5
+ options?: Record<string, unknown>;
6
+ }
7
+ export interface HermesConfig {
8
+ name?: string;
9
+ level: LogLevel;
10
+ format: LogFormat;
11
+ transports?: TransportConfig[];
12
+ /**
13
+ * Where log lines go: `pino`, or `console` for JSON lines through `console` with no
14
+ * dependency. Defaults to `console` on Cloudflare Workers and `pino` elsewhere.
15
+ */
16
+ transport?: 'pino' | 'console';
17
+ }
18
+ export declare const DEFAULT_CONFIG: HermesConfig;
package/package.json CHANGED
@@ -1,9 +1,23 @@
1
1
  {
2
2
  "name": "@elysiumjs/hermes",
3
- "version": "0.4.0",
3
+ "description": "Structured logging for the Elysium.js framework.",
4
+ "version": "0.5.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/na2axl/elysiumjs.git",
8
+ "directory": "@elysiumjs/hermes"
9
+ },
10
+ "homepage": "https://github.com/na2axl/elysiumjs/tree/main/@elysiumjs/hermes#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/na2axl/elysiumjs/issues"
13
+ },
4
14
  "type": "module",
5
15
  "module": "dist/index.js",
6
16
  "types": "dist/index.d.ts",
17
+ "files": [
18
+ "dist",
19
+ "CHANGELOG.md"
20
+ ],
7
21
  "sideEffects": false,
8
22
  "scripts": {
9
23
  "format": "prettier --write \"./src/**/*.ts\"",
@@ -21,9 +35,10 @@
21
35
  "pino-pretty": "^13.2.0"
22
36
  },
23
37
  "devDependencies": {
24
- "@elysiumjs/core": "0.8.0"
38
+ "@elysiumjs/core": "0.11.0",
39
+ "@elysiumjs/runtime-bun": "0.1.0"
25
40
  },
26
41
  "peerDependencies": {
27
- "@elysiumjs/core": "^0.8.0"
42
+ "@elysiumjs/core": "^0.11.0"
28
43
  }
29
44
  }
package/index.ts DELETED
@@ -1,15 +0,0 @@
1
- // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- export * from './src/index';
@@ -1,86 +0,0 @@
1
- // Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { LoggerInterface } from '@elysiumjs/core';
16
- import type { LogLevel } from '../types';
17
-
18
- import { ConsoleLogger } from '@elysiumjs/core';
19
-
20
- export interface LoggedOptions {
21
- level?: LogLevel;
22
- includeArgs?: boolean;
23
- includeResult?: boolean;
24
- includeTiming?: boolean;
25
- }
26
-
27
- /**
28
- * Decorator that logs method entry/exit with optional timing.
29
- *
30
- * Logger is resolved at CALL TIME (not decorator definition time)
31
- * to avoid capturing a stale logger reference.
32
- *
33
- * @author Axel Nana <axel.nana@workbud.com>
34
- */
35
- export function Logged(options: LoggedOptions = {}): MethodDecorator {
36
- return function (
37
- target: object,
38
- propertyKey: string | symbol,
39
- descriptor: TypedPropertyDescriptor<any>
40
- ) {
41
- const original = descriptor.value;
42
- const methodName = `${target.constructor.name}.${String(propertyKey)}`;
43
-
44
- descriptor.value = async function (...args: any[]) {
45
- const logger: LoggerInterface = (this as any).logger ?? getServiceLogger();
46
-
47
- const startTime = Date.now();
48
-
49
- if (options.includeArgs) {
50
- logger.debug(`[${methodName}] Entry`, { args });
51
- }
52
-
53
- try {
54
- const result = await original.apply(this, args);
55
-
56
- if (options.includeResult || options.includeTiming) {
57
- const duration = Date.now() - startTime;
58
- logger[options.level ?? 'debug'](`[${methodName}] Exit`, {
59
- ...(options.includeResult && { result }),
60
- ...(options.includeTiming && { duration: `${duration}ms` })
61
- });
62
- }
63
-
64
- return result;
65
- } catch (error) {
66
- const duration = Date.now() - startTime;
67
- logger.error(`[${methodName}] Error`, {
68
- error: error instanceof Error ? error.message : error,
69
- duration: `${duration}ms`
70
- });
71
- throw error;
72
- }
73
- };
74
-
75
- return descriptor;
76
- };
77
- }
78
-
79
- function getServiceLogger(): LoggerInterface {
80
- try {
81
- const { Service } = require('@elysiumjs/core');
82
- return Service.get('logger') ?? new ConsoleLogger();
83
- } catch {
84
- return new ConsoleLogger();
85
- }
86
- }
@@ -1,37 +0,0 @@
1
- // Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import { Module } from '@elysiumjs/core';
16
-
17
- import { HermesLogger } from './services/logger.service';
18
- import { DEFAULT_CONFIG, type HermesConfig } from './types';
19
-
20
- /**
21
- * Provides the {@link HermesLogger} as the global services `HermesLogger` and `logger`
22
- * (the same instance), built from the module config.
23
- * @author Axel Nana <axel.nana@workbud.com>
24
- */
25
- @Module.register<HermesConfig>({
26
- services: (config) => [
27
- { name: HermesLogger.name, global: true, factory: () => new HermesLogger(config) },
28
- {
29
- name: 'logger',
30
- global: true,
31
- factory: (_config: unknown, get: <T>(name: string) => T) => get<HermesLogger>(HermesLogger.name)
32
- }
33
- ]
34
- })
35
- export class HermesModule extends Module.configurable<HermesConfig>({
36
- defaults: { ...DEFAULT_CONFIG }
37
- }) {}
package/src/index.ts DELETED
@@ -1,21 +0,0 @@
1
- // Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- export { HermesModule } from './hermes.module';
16
- export { HermesLogger } from './services/logger.service';
17
- export { HttpTransport } from './transports/http.transport';
18
- export type { HttpTransportOptions } from './transports/http.transport';
19
- export { Logged } from './decorators/logged';
20
- export type { LoggedOptions } from './decorators/logged';
21
- export type { HermesConfig, LogLevel, LogFormat, TransportConfig } from './types';
@@ -1,98 +0,0 @@
1
- // Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { LoggerInterface } from '@elysiumjs/core';
16
- import type { Logger as PinoLogger } from 'pino';
17
- import type { HermesConfig } from '../types';
18
-
19
- import { Service } from '@elysiumjs/core';
20
- import pino from 'pino';
21
-
22
- import { DEFAULT_CONFIG } from '../types';
23
-
24
- /**
25
- * Production-ready logger implementation wrapping Pino.
26
- * Implements `LoggerInterface` from `@elysiumjs/core`.
27
- *
28
- * @author Axel Nana <axel.nana@workbud.com>
29
- */
30
- export class HermesLogger implements LoggerInterface {
31
- private pino: PinoLogger;
32
- private config: HermesConfig;
33
-
34
- public static make(name: string): HermesLogger {
35
- return Service.get(HermesLogger)?.child({ name }) ?? new HermesLogger({ name });
36
- }
37
-
38
- constructor(config: Partial<HermesConfig> = {}) {
39
- this.config = { ...DEFAULT_CONFIG, ...config };
40
- this.pino = this.createPinoInstance();
41
- }
42
-
43
- private createPinoInstance(): PinoLogger {
44
- const options: pino.LoggerOptions = {
45
- level: this.config.level,
46
- name: this.config.name
47
- };
48
-
49
- if (this.config.format === 'pretty') {
50
- return pino(options, pino.transport({ target: 'pino-pretty' }));
51
- }
52
-
53
- return pino(options);
54
- }
55
-
56
- public trace(msg: string, ctx?: Record<string, unknown>): void {
57
- this.pino.trace(ctx ?? {}, msg);
58
- }
59
-
60
- public debug(msg: string, ctx?: Record<string, unknown>): void {
61
- this.pino.debug(ctx ?? {}, msg);
62
- }
63
-
64
- public info(msg: string, ctx?: Record<string, unknown>): void {
65
- this.pino.info(ctx ?? {}, msg);
66
- }
67
-
68
- public warn(msg: string, ctx?: Record<string, unknown>): void {
69
- this.pino.warn(ctx ?? {}, msg);
70
- }
71
-
72
- public error(msg: string, ctx?: Record<string, unknown>): void {
73
- this.pino.error(ctx ?? {}, msg);
74
- }
75
-
76
- public fatal(msg: string, ctx?: Record<string, unknown>): void {
77
- this.pino.fatal(ctx ?? {}, msg);
78
- }
79
-
80
- /**
81
- * Flushes buffered log lines of the underlying transport, when it supports it.
82
- * @author Axel Nana <axel.nana@workbud.com>
83
- */
84
- public flush(): void {
85
- this.pino.flush?.();
86
- }
87
-
88
- /** Flushes on shutdown. Only called for a logger the container built. */
89
- public onDestroy(): void {
90
- this.flush();
91
- }
92
-
93
- public child(ctx: Record<string, unknown>): HermesLogger {
94
- const child = Object.create(this) as HermesLogger;
95
- child.pino = this.pino.child(ctx);
96
- return child;
97
- }
98
- }
@@ -1,79 +0,0 @@
1
- // Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- export interface HttpTransportOptions {
16
- url: string;
17
- headers?: Record<string, string>;
18
- batchSize?: number;
19
- flushInterval?: number;
20
- maxBufferSize?: number;
21
- }
22
-
23
- /**
24
- * HTTP transport for batching and sending logs to a remote endpoint.
25
- *
26
- * @author Axel Nana <axel.nana@workbud.com>
27
- */
28
- export class HttpTransport {
29
- private buffer: string[] = [];
30
- private flushTimer?: ReturnType<typeof setInterval>;
31
- private maxBufferSize: number;
32
-
33
- constructor(private options: HttpTransportOptions) {
34
- this.options.batchSize ??= 100;
35
- this.options.flushInterval ??= 5000;
36
- this.maxBufferSize = this.options.maxBufferSize ?? 10_000;
37
-
38
- this.flushTimer = setInterval(() => this.flush(), this.options.flushInterval);
39
- }
40
-
41
- public write(formatted: string): void {
42
- if (this.buffer.length >= this.maxBufferSize) {
43
- this.buffer.shift();
44
- }
45
-
46
- this.buffer.push(formatted);
47
-
48
- if (this.buffer.length >= this.options.batchSize!) {
49
- this.flush();
50
- }
51
- }
52
-
53
- public async flush(): Promise<void> {
54
- if (this.buffer.length === 0) return;
55
-
56
- const batch = this.buffer.splice(0);
57
-
58
- try {
59
- await fetch(this.options.url, {
60
- method: 'POST',
61
- headers: {
62
- 'Content-Type': 'application/json',
63
- ...this.options.headers
64
- },
65
- body: JSON.stringify({ logs: batch })
66
- });
67
- } catch {
68
- const space = this.maxBufferSize - this.buffer.length;
69
- if (space > 0) {
70
- this.buffer.unshift(...batch.slice(0, space));
71
- }
72
- }
73
- }
74
-
75
- public async close(): Promise<void> {
76
- if (this.flushTimer) clearInterval(this.flushTimer);
77
- await this.flush();
78
- }
79
- }
package/src/types.ts DELETED
@@ -1,34 +0,0 @@
1
- // Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
16
-
17
- export type LogFormat = 'json' | 'pretty' | 'logfmt';
18
-
19
- export interface TransportConfig {
20
- type: 'console' | 'file' | 'http';
21
- options?: Record<string, unknown>;
22
- }
23
-
24
- export interface HermesConfig {
25
- name?: string;
26
- level: LogLevel;
27
- format: LogFormat;
28
- transports?: TransportConfig[];
29
- }
30
-
31
- export const DEFAULT_CONFIG: HermesConfig = {
32
- level: 'info',
33
- format: 'json'
34
- };