@elysiumjs/hermes 0.1.1 → 0.3.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.
@@ -0,0 +1,8 @@
1
+ $ bun run clean && bun run build:js && bun run build:ts
2
+ $ rimraf dist
3
+ $ bun build ./src/index.ts --outdir ./dist --target bun --packages external --root ./src --minify
4
+ Bundled 5 modules in 9ms
5
+
6
+ index.js 2.52 KB (entry point)
7
+
8
+ $ tsc
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # @elysiumjs/hermes
2
+
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add HTTP transport for remote log shipping.
8
+ - Add `@Logged` decorator for automatic method-level logging.
9
+ - Implement lazy DI registration and logger factory pattern.
10
+ - Export new types and improve overall logging API.
package/README.md CHANGED
@@ -11,9 +11,9 @@ bun add @elysiumjs/hermes
11
11
  ## Usage
12
12
 
13
13
  ```ts
14
- import { LoggerService } from '@elysiumjs/hermes';
14
+ import { HermesLogger } from '@elysiumjs/hermes';
15
15
 
16
- const logger = LoggerService.make('my-logger');
16
+ const logger = HermesLogger.make('my-logger');
17
17
 
18
18
  logger.info('Hello world!');
19
19
  logger.error('Something went wrong!');
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // @bun
2
+ var V=import.meta.require;import{Service as S}from"@elysiumjs/core";import{Service as G}from"@elysiumjs/core";import E from"pino";var Y={level:"info",format:"json"};class C{pino;config;static make(f){return G.get(C)?.child({name:f})??new C({name:f})}constructor(f={}){this.config={...Y,...f},this.pino=this.createPinoInstance()}createPinoInstance(){let f={level:this.config.level,name:this.config.name};if(this.config.format==="pretty")return E(f,E.transport({target:"pino-pretty"}));return E(f)}trace(f,O){this.pino.trace(O??{},f)}debug(f,O){this.pino.debug(O??{},f)}info(f,O){this.pino.info(O??{},f)}warn(f,O){this.pino.warn(O??{},f)}error(f,O){this.pino.error(O??{},f)}fatal(f,O){this.pino.fatal(O??{},f)}child(f){let O=Object.create(this);return O.pino=this.pino.child(f),O}}class b{options;buffer=[];flushTimer;maxBufferSize;constructor(f){this.options=f;this.options.batchSize??=100,this.options.flushInterval??=5000,this.maxBufferSize=this.options.maxBufferSize??1e4,this.flushTimer=setInterval(()=>this.flush(),this.options.flushInterval)}write(f){if(this.buffer.length>=this.maxBufferSize)this.buffer.shift();if(this.buffer.push(f),this.buffer.length>=this.options.batchSize)this.flush()}async flush(){if(this.buffer.length===0)return;let f=this.buffer.splice(0);try{await fetch(this.options.url,{method:"POST",headers:{"Content-Type":"application/json",...this.options.headers},body:JSON.stringify({logs:f})})}catch{let O=this.maxBufferSize-this.buffer.length;if(O>0)this.buffer.unshift(...f.slice(0,O))}}async close(){if(this.flushTimer)clearInterval(this.flushTimer);await this.flush()}}import{ConsoleLogger as q}from"@elysiumjs/core";function J(f={}){return function(O,D,F){let L=F.value,I=`${O.constructor.name}.${String(D)}`;return F.value=async function(...T){let W=this.logger??N(),U=Date.now();if(f.includeArgs)W.debug(`[${I}] Entry`,{args:T});try{let w=await L.apply(this,T);if(f.includeResult||f.includeTiming){let A=Date.now()-U;W[f.level??"debug"](`[${I}] Exit`,{...f.includeResult&&{result:w},...f.includeTiming&&{duration:`${A}ms`}})}return w}catch(w){let A=Date.now()-U;throw W.error(`[${I}] Error`,{error:w instanceof Error?w.message:w,duration:`${A}ms`}),w}},F}}function N(){try{let{Service:f}=V("@elysiumjs/core");return f.get("logger")??new q}catch{return new q}}var R=null,B=()=>{if(!R){let O=S.get("elysium.app")?.getConfig("elysium:hermes");R=new C(O??{})}return R};S.registerLazy(C.name,B);S.registerLazy("logger",B);export{J as Logged,b as HttpTransport,C as HermesLogger};
package/index.ts CHANGED
@@ -12,6 +12,4 @@
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
14
 
15
- import './src/services/pino.factory';
16
-
17
- export { LoggerService } from './src/services/logger.service.ts';
15
+ export * from './src/index';
package/package.json CHANGED
@@ -1,18 +1,29 @@
1
1
  {
2
2
  "name": "@elysiumjs/hermes",
3
- "version": "0.1.1",
4
- "module": "index.ts",
3
+ "version": "0.3.0",
5
4
  "type": "module",
5
+ "module": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "sideEffects": ["./src/index.ts"],
8
+ "scripts": {
9
+ "format": "prettier --write \"./src/**/*.ts\"",
10
+ "clean": "rimraf dist",
11
+ "build:js": "bun build ./src/index.ts --outdir ./dist --target bun --packages external --root ./src --minify",
12
+ "build:ts": "tsc",
13
+ "build": "bun run clean && bun run build:js && bun run build:ts",
14
+ "test": "bun test",
15
+ "test:coverage": "bun test --coverage",
16
+ "typecheck": "tsc --noEmit",
17
+ "prepublishOnly": "bun run build"
18
+ },
19
+ "dependencies": {
20
+ "pino": "^10.0.0",
21
+ "pino-pretty": "^13.0.0"
22
+ },
6
23
  "devDependencies": {
7
- "@ianvs/prettier-plugin-sort-imports": "^4.4.1",
8
- "@types/bun": "latest",
9
- "type-fest": "^4.41.0"
24
+ "@elysiumjs/core": "workspace:*"
10
25
  },
11
26
  "peerDependencies": {
12
- "@elysiumjs/core": "latest",
13
- "typescript": "^5.8.3"
14
- },
15
- "dependencies": {
16
- "pino": "^9.6.0"
27
+ "@elysiumjs/core": "workspace:^"
17
28
  }
18
29
  }
@@ -0,0 +1,86 @@
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
+ }
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
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 { HermesConfig } from './types';
16
+
17
+ import { Application, Service } from '@elysiumjs/core';
18
+
19
+ import { HermesLogger } from './services/logger.service';
20
+
21
+ // Lazy singleton resolver: reads config from Application on first access.
22
+ let hermesInstance: HermesLogger | null = null;
23
+ const hermesResolver = () => {
24
+ if (!hermesInstance) {
25
+ const app = Service.get<Application>('elysium.app');
26
+ const config = app?.getConfig<Partial<HermesConfig>>('elysium:hermes');
27
+ hermesInstance = new HermesLogger(config ?? {});
28
+ }
29
+ return hermesInstance;
30
+ };
31
+
32
+ Service.registerLazy(HermesLogger.name, hermesResolver);
33
+ Service.registerLazy('logger', hermesResolver);
34
+
35
+ export { HermesLogger } from './services/logger.service';
36
+ export { HttpTransport } from './transports/http.transport';
37
+ export type { HttpTransportOptions } from './transports/http.transport';
38
+ export { Logged } from './decorators/logged';
39
+ export type { LoggedOptions } from './decorators/logged';
40
+ export type { HermesConfig, LogLevel, LogFormat, TransportConfig } from './types';
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
1
+ // Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
2
2
  //
3
3
  // Licensed under the Apache License, Version 2.0 (the "License");
4
4
  // you may not use this file except in compliance with the License.
@@ -12,52 +12,74 @@
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
14
 
15
- import type { Logger } from 'pino';
15
+ import type { LoggerInterface } from '@elysiumjs/core';
16
+ import type { Logger as PinoLogger } from 'pino';
17
+ import type { HermesConfig } from '../types';
16
18
 
17
- import { Service, ServiceScope } from '@elysiumjs/core';
19
+ import { Service } from '@elysiumjs/core';
18
20
  import pino from 'pino';
19
21
 
20
- @Service.register({ scope: ServiceScope.SINGLETON })
21
- export class LoggerService {
22
- readonly #pino: Logger;
22
+ import { DEFAULT_CONFIG } from '../types';
23
23
 
24
- public constructor(@Service.inject('elysium.hermes.pino') pino: pino.Logger) {
25
- this.#pino = pino;
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 });
26
36
  }
27
37
 
28
- public static make(name: string) {
29
- return new LoggerService(pino({ name }));
38
+ constructor(config: Partial<HermesConfig> = {}) {
39
+ this.config = { ...DEFAULT_CONFIG, ...config };
40
+ this.pino = this.createPinoInstance();
30
41
  }
31
42
 
32
- public info(message: string, details?: Record<string, unknown>) {
33
- if (details) {
34
- this.#pino.info(details, message);
35
- } else {
36
- this.#pino.info(message);
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' }));
37
51
  }
52
+
53
+ return pino(options);
38
54
  }
39
55
 
40
- public error(message: string, details?: Record<string, unknown>) {
41
- if (details) {
42
- this.#pino.error(details, message);
43
- } else {
44
- this.#pino.error(message);
45
- }
56
+ public trace(msg: string, ctx?: Record<string, unknown>): void {
57
+ this.pino.trace(ctx ?? {}, msg);
46
58
  }
47
59
 
48
- public warn(message: string, details?: Record<string, unknown>) {
49
- if (details) {
50
- this.#pino.error(details, message);
51
- } else {
52
- this.#pino.warn(message);
53
- }
60
+ public debug(msg: string, ctx?: Record<string, unknown>): void {
61
+ this.pino.debug(ctx ?? {}, msg);
54
62
  }
55
63
 
56
- public debug(message: string, details?: Record<string, unknown>) {
57
- if (details) {
58
- this.#pino.error(details, message);
59
- } else {
60
- this.#pino.debug(message);
61
- }
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
+ public child(ctx: Record<string, unknown>): HermesLogger {
81
+ const child = Object.create(this) as HermesLogger;
82
+ child.pino = this.pino.child(ctx);
83
+ return child;
62
84
  }
63
85
  }
@@ -0,0 +1,79 @@
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
+ }
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
1
+ // Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
2
2
  //
3
3
  // Licensed under the Apache License, Version 2.0 (the "License");
4
4
  // you may not use this file except in compliance with the License.
@@ -12,9 +12,23 @@
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
14
 
15
- import { Service } from '@elysiumjs/core';
16
- import pino from 'pino';
15
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
17
16
 
18
- const pinoFactory = () => pino();
17
+ export type LogFormat = 'json' | 'pretty' | 'logfmt';
19
18
 
20
- Service.factory('elysium.hermes.pino', pinoFactory);
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
+ };