@elysiumjs/hermes 0.4.3 → 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,39 @@
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
+
3
37
  ## 0.4.3
4
38
 
5
39
  ### Patch 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';
@@ -1,9 +1,9 @@
1
- import { type HermesConfig } from './types';
1
+ import type { HermesConfig } from './types';
2
2
  declare const HermesModule_base: (abstract new () => {
3
3
  readonly config: HermesConfig;
4
4
  onInit(): Promise<void> | void;
5
5
  onDestroy(): Promise<void> | void;
6
- onServe(_app: import("elysia").default<any>): Promise<void> | void;
6
+ onServe(_app: import("elysia").Elysia<any>): Promise<void> | void;
7
7
  }) & {
8
8
  readonly config: string;
9
9
  with(config: Partial<HermesConfig> | (() => Partial<HermesConfig> | Promise<Partial<HermesConfig>>)): import("@elysiumjs/core").ConfiguredModule<HermesConfig>;
@@ -15,8 +15,9 @@ declare const HermesModule_base: (abstract new () => {
15
15
  }): any;
16
16
  };
17
17
  /**
18
- * Provides the {@link HermesLogger} as the global services `HermesLogger` and `logger`
19
- * (the same instance), built from the module config.
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.
20
21
  * @author Axel Nana <axel.nana@workbud.com>
21
22
  */
22
23
  export declare class HermesModule extends HermesModule_base {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { HermesModule } from './hermes.module';
2
+ export { RequestLogInterceptor } from './interceptors/request-log.interceptor';
3
+ export { JsonConsoleLogger } from './services/json-console.logger';
2
4
  export { HermesLogger } from './services/logger.service';
3
5
  export { HttpTransport } from './transports/http.transport';
4
6
  export type { HttpTransportOptions } from './transports/http.transport';
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};import{LOGGER_SERVICE,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,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}}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_SERVICE,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{getLogger}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??getLogger(),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,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
+ }
package/dist/types.d.ts CHANGED
@@ -9,5 +9,10 @@ export interface HermesConfig {
9
9
  level: LogLevel;
10
10
  format: LogFormat;
11
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';
12
17
  }
13
18
  export declare const DEFAULT_CONFIG: HermesConfig;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@elysiumjs/hermes",
3
- "version": "0.4.3",
3
+ "description": "Structured logging for the Elysium.js framework.",
4
+ "version": "0.5.0",
4
5
  "repository": {
5
6
  "type": "git",
6
7
  "url": "git+https://github.com/na2axl/elysiumjs.git",
@@ -34,9 +35,10 @@
34
35
  "pino-pretty": "^13.2.0"
35
36
  },
36
37
  "devDependencies": {
37
- "@elysiumjs/core": "0.10.0"
38
+ "@elysiumjs/core": "0.11.0",
39
+ "@elysiumjs/runtime-bun": "0.1.0"
38
40
  },
39
41
  "peerDependencies": {
40
- "@elysiumjs/core": "^0.10.0"
42
+ "@elysiumjs/core": "^0.11.0"
41
43
  }
42
44
  }