@smounters/imperium 1.1.3 → 1.2.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
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to `@smounters/imperium` are documented in this file.
4
4
 
5
+ ## 1.2.0 - 2026-03-30
6
+
7
+ ### Added
8
+ - **Pluggable logger transports** — `LogTransport` interface, `ImperiumLogger` class, `consoleTransport()` factory.
9
+ - **Global `onError` callback** — single error reporting point for HTTP, RPC, WebSocket, cron, and event handler errors. Integrates with Sentry, OTLP, or any custom reporter.
10
+ - New types: `LogLevel`, `LogEntry`, `LogTransport`, `ImperiumLoggerOptions`, `ErrorContext`, `ErrorContextType`, `OnErrorCallback`.
11
+
12
+ ### Changed
13
+ - `LoggerOptions` is now a union of `ImperiumLoggerOptions` (transport-based) and `TslogOptions` (legacy tslog). Auto-detected from options shape.
14
+ - All adapters (HTTP, RPC, WS) now call `reportError()` when `onError` is configured.
15
+ - `imperium-cron` and `imperium-events` now use `LoggerService` and `onError` instead of raw `console.error`.
16
+
5
17
  ## 1.1.3 - 2026-03-30
6
18
 
7
19
  ### Added
@@ -1,6 +1,6 @@
1
1
  import "reflect-metadata";
2
2
  import { type DependencyContainer } from "tsyringe";
3
- import type { Constructor, ExceptionFilterLike, GuardLike, InjectionToken, InterceptorLike, LoggerOptions, ModuleImport, PipeLike } from "../types.js";
3
+ import type { Constructor, ExceptionFilterLike, GuardLike, InjectionToken, InterceptorLike, LoggerOptions, ModuleImport, OnErrorCallback, ErrorContext, PipeLike } from "../types.js";
4
4
  import type { ZodType, output } from "zod";
5
5
  import { type AppLogger } from "./logger.js";
6
6
  export declare class AppContainer {
@@ -27,6 +27,7 @@ export declare class AppContainer {
27
27
  private initialized;
28
28
  private closed;
29
29
  private exposeInternalErrors;
30
+ private onErrorCallback;
30
31
  private globalGuards;
31
32
  private globalInterceptors;
32
33
  private globalPipes;
@@ -58,6 +59,9 @@ export declare class AppContainer {
58
59
  getLogger(): AppLogger;
59
60
  setExposeInternalErrors(value: boolean): void;
60
61
  shouldExposeInternalErrors(): boolean;
62
+ setOnError(callback: OnErrorCallback): void;
63
+ getOnError(): OnErrorCallback | undefined;
64
+ reportError(error: unknown, context: ErrorContext): void;
61
65
  setConfig<TConfig extends Record<string, unknown>>(config: TConfig): void;
62
66
  configureConfig<TSchema extends ZodType>(schema: TSchema, source?: unknown): output<TSchema>;
63
67
  getConfig<TConfig extends Record<string, unknown> = Record<string, unknown>>(): Readonly<TConfig>;
@@ -507,6 +507,22 @@ export class AppContainer {
507
507
  shouldExposeInternalErrors() {
508
508
  return this.exposeInternalErrors;
509
509
  }
510
+ setOnError(callback) {
511
+ this.onErrorCallback = callback;
512
+ }
513
+ getOnError() {
514
+ return this.onErrorCallback;
515
+ }
516
+ reportError(error, context) {
517
+ if (!this.onErrorCallback)
518
+ return;
519
+ try {
520
+ void this.onErrorCallback(error, context);
521
+ }
522
+ catch {
523
+ // never let error reporter break the app
524
+ }
525
+ }
510
526
  setConfig(config) {
511
527
  this.assertRuntimeConfigMutable("config");
512
528
  this.root.registerInstance(CONFIG_TOKEN, config);
@@ -0,0 +1,20 @@
1
+ import type { ImperiumLoggerOptions } from "../types.js";
2
+ /**
3
+ * Native imperium logger with pluggable transports.
4
+ * Drop-in replacement for tslog Logger — same method signatures.
5
+ */
6
+ export declare class ImperiumLogger {
7
+ private readonly transports;
8
+ private readonly minIdx;
9
+ private readonly name?;
10
+ constructor(options?: ImperiumLoggerOptions);
11
+ private dispatch;
12
+ log(_logLevelId: number, logLevelName: string, ...args: unknown[]): void;
13
+ silly(...args: unknown[]): void;
14
+ trace(...args: unknown[]): void;
15
+ debug(...args: unknown[]): void;
16
+ info(...args: unknown[]): void;
17
+ warn(...args: unknown[]): void;
18
+ error(...args: unknown[]): void;
19
+ fatal(...args: unknown[]): void;
20
+ }
@@ -0,0 +1,59 @@
1
+ import { consoleTransport, levelIndex } from "./log-transport.js";
2
+ /**
3
+ * Native imperium logger with pluggable transports.
4
+ * Drop-in replacement for tslog Logger — same method signatures.
5
+ */
6
+ export class ImperiumLogger {
7
+ constructor(options = {}) {
8
+ this.transports = options.transports ?? [consoleTransport()];
9
+ this.minIdx = levelIndex(options.minLevel ?? "silly");
10
+ this.name = options.name;
11
+ }
12
+ dispatch(level, args) {
13
+ const idx = levelIndex(level);
14
+ if (idx < this.minIdx)
15
+ return;
16
+ const message = args.length > 0 && typeof args[0] === "string" ? args[0] : "";
17
+ const rest = typeof args[0] === "string" ? args.slice(1) : args;
18
+ const entry = {
19
+ level,
20
+ message,
21
+ args: rest,
22
+ timestamp: new Date(),
23
+ name: this.name,
24
+ };
25
+ for (const transport of this.transports) {
26
+ try {
27
+ transport.log(entry);
28
+ }
29
+ catch {
30
+ // never let a broken transport crash the app
31
+ }
32
+ }
33
+ }
34
+ log(_logLevelId, logLevelName, ...args) {
35
+ const level = logLevelName.toLowerCase();
36
+ this.dispatch(level, args);
37
+ }
38
+ silly(...args) {
39
+ this.dispatch("silly", args);
40
+ }
41
+ trace(...args) {
42
+ this.dispatch("trace", args);
43
+ }
44
+ debug(...args) {
45
+ this.dispatch("debug", args);
46
+ }
47
+ info(...args) {
48
+ this.dispatch("info", args);
49
+ }
50
+ warn(...args) {
51
+ this.dispatch("warn", args);
52
+ }
53
+ error(...args) {
54
+ this.dispatch("error", args);
55
+ }
56
+ fatal(...args) {
57
+ this.dispatch("fatal", args);
58
+ }
59
+ }
@@ -2,4 +2,6 @@ export { Application } from "./application.js";
2
2
  export { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from "./app-tokens.js";
3
3
  export { BadRequestException, ForbiddenException, HttpException, InternalServerErrorException, NotFoundException, UnauthorizedException, } from "./errors.js";
4
4
  export { Reflector } from "./reflector.js";
5
+ export { consoleTransport } from "./log-transport.js";
6
+ export { ImperiumLogger } from "./imperium-logger.js";
5
7
  export type * from "../types.js";
@@ -2,3 +2,5 @@ export { Application } from "./application.js";
2
2
  export { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from "./app-tokens.js";
3
3
  export { BadRequestException, ForbiddenException, HttpException, InternalServerErrorException, NotFoundException, UnauthorizedException, } from "./errors.js";
4
4
  export { Reflector } from "./reflector.js";
5
+ export { consoleTransport } from "./log-transport.js";
6
+ export { ImperiumLogger } from "./imperium-logger.js";
@@ -0,0 +1,11 @@
1
+ import type { ImperiumLoggerOptions, LogLevel, LogTransport, TslogOptions } from "../types.js";
2
+ export declare const LOG_LEVELS: LogLevel[];
3
+ export declare function levelIndex(level: LogLevel): number;
4
+ export declare function isImperiumLoggerOptions(options: unknown): options is ImperiumLoggerOptions;
5
+ export declare function isTslogOptions(options: unknown): options is TslogOptions;
6
+ /**
7
+ * Built-in console transport. Writes to stdout (silly..info) and stderr (warn..fatal).
8
+ */
9
+ export declare function consoleTransport(options?: {
10
+ minLevel?: LogLevel;
11
+ }): LogTransport;
@@ -0,0 +1,51 @@
1
+ export const LOG_LEVELS = ["silly", "trace", "debug", "info", "warn", "error", "fatal"];
2
+ export function levelIndex(level) {
3
+ return LOG_LEVELS.indexOf(level);
4
+ }
5
+ export function isImperiumLoggerOptions(options) {
6
+ if (!options || typeof options !== "object") {
7
+ return false;
8
+ }
9
+ const obj = options;
10
+ // ImperiumLoggerOptions has 'transports' or 'minLevel' as LogLevel string
11
+ if (Array.isArray(obj.transports)) {
12
+ return true;
13
+ }
14
+ if (typeof obj.minLevel === "string" && LOG_LEVELS.includes(obj.minLevel)) {
15
+ // Could be tslog too (it also has minLevel as number), but string minLevel is ours
16
+ if (typeof obj.type !== "string") {
17
+ return true;
18
+ }
19
+ }
20
+ return false;
21
+ }
22
+ export function isTslogOptions(options) {
23
+ return !isImperiumLoggerOptions(options);
24
+ }
25
+ function formatTimestamp(date) {
26
+ return date.toISOString().replace("T", " ").replace("Z", "");
27
+ }
28
+ function formatLevel(level) {
29
+ return level.toUpperCase().padEnd(5);
30
+ }
31
+ /**
32
+ * Built-in console transport. Writes to stdout (silly..info) and stderr (warn..fatal).
33
+ */
34
+ export function consoleTransport(options) {
35
+ const minIdx = levelIndex(options?.minLevel ?? "silly");
36
+ return {
37
+ log(entry) {
38
+ const idx = levelIndex(entry.level);
39
+ if (idx < minIdx)
40
+ return;
41
+ const prefix = `${formatTimestamp(entry.timestamp)} | ${formatLevel(entry.level)} |${entry.name ? ` ${entry.name} |` : ""}`;
42
+ const parts = [prefix, entry.message, ...entry.args];
43
+ if (idx >= levelIndex("warn")) {
44
+ console.error(...parts);
45
+ }
46
+ else {
47
+ console.log(...parts);
48
+ }
49
+ },
50
+ };
51
+ }
@@ -1,7 +1,8 @@
1
1
  import { Logger } from "tslog";
2
2
  import type { LoggerOptions } from "../types.js";
3
+ import { ImperiumLogger } from "./imperium-logger.js";
3
4
  type LoggerPayload = Record<string, unknown>;
4
- export type AppLogger = Logger<LoggerPayload>;
5
+ export type AppLogger = Logger<LoggerPayload> | ImperiumLogger;
5
6
  export declare const LOGGER_TOKEN: unique symbol;
6
7
  export declare function createLogger(options?: LoggerOptions): AppLogger;
7
8
  export {};
@@ -1,12 +1,17 @@
1
1
  import { Logger } from "tslog";
2
+ import { ImperiumLogger } from "./imperium-logger.js";
3
+ import { isImperiumLoggerOptions } from "./log-transport.js";
2
4
  export const LOGGER_TOKEN = Symbol("app:logger");
3
- const DEFAULT_LOGGER_OPTIONS = {
5
+ const DEFAULT_TSLOG_OPTIONS = {
4
6
  name: "app",
5
7
  type: "pretty",
6
8
  };
7
9
  export function createLogger(options) {
10
+ if (options && isImperiumLoggerOptions(options)) {
11
+ return new ImperiumLogger(options);
12
+ }
8
13
  return new Logger({
9
- ...DEFAULT_LOGGER_OPTIONS,
14
+ ...DEFAULT_TSLOG_OPTIONS,
10
15
  ...(options ?? {}),
11
16
  });
12
17
  }
@@ -140,7 +140,7 @@ export async function startServer(diOrOptions, options) {
140
140
  if (!serverOptions) {
141
141
  throw new Error("Server options are required");
142
142
  }
143
- const { port, host = "0.0.0.0", prefix, httpPrefix, rpcPrefix, trustProxy, requestTimeout, connectionTimeout, keepAliveTimeout, bodyLimit, routerOptions, maxParamLength: legacyMaxParamLength, pluginTimeout, accessLogs = false, exposeInternalErrors = false, cors, health, loggerOptions, config, } = serverOptions;
143
+ const { port, host = "0.0.0.0", prefix, httpPrefix, rpcPrefix, trustProxy, requestTimeout, connectionTimeout, keepAliveTimeout, bodyLimit, routerOptions, maxParamLength: legacyMaxParamLength, pluginTimeout, accessLogs = false, exposeInternalErrors = false, cors, health, loggerOptions, onError, config, } = serverOptions;
144
144
  if (loggerOptions !== undefined) {
145
145
  di.configureLogger(loggerOptions);
146
146
  }
@@ -148,6 +148,9 @@ export async function startServer(diOrOptions, options) {
148
148
  di.configureConfig(config.schema, config.source ?? process.env);
149
149
  }
150
150
  di.setExposeInternalErrors(exposeInternalErrors);
151
+ if (onError) {
152
+ di.setOnError(onError);
153
+ }
151
154
  const listenPort = resolveListenPort(port, di.getConfig());
152
155
  const healthConfig = resolveHealth(health);
153
156
  const http = hasRegisteredHttpHandlers(di);
@@ -6,19 +6,22 @@ import { collectFiltersForHttp, collectGuardsForHttp, collectInterceptorsForHttp
6
6
  function logHttpError(app, scope, details, error) {
7
7
  try {
8
8
  scope.resolve(LoggerService).error(details, error);
9
- return;
10
9
  }
11
10
  catch {
12
- // fallback below
13
- }
14
- try {
15
- app.getLogger().error(details, error);
16
- return;
17
- }
18
- catch {
19
- // final fallback
11
+ try {
12
+ app.getLogger().error(details, error);
13
+ }
14
+ catch {
15
+ console.error("[imperium] http_error", details, error);
16
+ }
20
17
  }
21
- console.error("[imperium] http_error", details, error);
18
+ app.reportError(error, {
19
+ type: "http",
20
+ handler: details.handler,
21
+ controller: details.controller,
22
+ method: details.method,
23
+ url: details.url,
24
+ });
22
25
  }
23
26
  function resolveEnhancer(scope, enhancer) {
24
27
  if (typeof enhancer === "function") {
@@ -7,19 +7,21 @@ import { collectFiltersForRpc, collectGuardsForRpc, collectInterceptorsForRpc, c
7
7
  function logRpcError(app, scope, details, error) {
8
8
  try {
9
9
  scope.resolve(LoggerService).error(details, error);
10
- return;
11
10
  }
12
11
  catch {
13
- // fallback below
14
- }
15
- try {
16
- app.getLogger().error(details, error);
17
- return;
18
- }
19
- catch {
20
- // final fallback
12
+ try {
13
+ app.getLogger().error(details, error);
14
+ }
15
+ catch {
16
+ console.error("[imperium] rpc_error", details, error);
17
+ }
21
18
  }
22
- console.error("[imperium] rpc_error", details, error);
19
+ app.reportError(error, {
20
+ type: "rpc",
21
+ handler: details.handler,
22
+ controller: details.controller,
23
+ procedure: details.procedure,
24
+ });
23
25
  }
24
26
  function getPayloadValue(payload, key) {
25
27
  if (!key) {
@@ -7,19 +7,21 @@ import { collectFiltersForRpc, collectGuardsForRpc, collectPipesForRpc } from ".
7
7
  function logRpcError(app, scope, details, error) {
8
8
  try {
9
9
  scope.resolve(LoggerService).error(details, error);
10
- return;
11
10
  }
12
11
  catch {
13
- // fallback below
14
- }
15
- try {
16
- app.getLogger().error(details, error);
17
- return;
18
- }
19
- catch {
20
- // final fallback
12
+ try {
13
+ app.getLogger().error(details, error);
14
+ }
15
+ catch {
16
+ console.error("[imperium] rpc_streaming_error", details, error);
17
+ }
21
18
  }
22
- console.error("[imperium] rpc_streaming_error", details, error);
19
+ app.reportError(error, {
20
+ type: "rpc",
21
+ handler: details.handler,
22
+ controller: details.controller,
23
+ procedure: details.procedure,
24
+ });
23
25
  }
24
26
  function getPayloadValue(payload, key) {
25
27
  if (!key) {
@@ -3,28 +3,28 @@ export declare class LoggerService {
3
3
  private readonly logger;
4
4
  constructor(logger: AppLogger);
5
5
  getRawLogger(): AppLogger;
6
- log(logLevelId: number, logLevelName: string, ...args: unknown[]): ({
6
+ log(logLevelId: number, logLevelName: string, ...args: unknown[]): void | ({
7
7
  [x: string]: unknown;
8
- } & import("tslog").ILogObjMeta & import("tslog").ILogObj) | undefined;
9
- silly(...args: unknown[]): ({
8
+ } & import("tslog").ILogObjMeta & import("tslog").ILogObj);
9
+ silly(...args: unknown[]): void | ({
10
10
  [x: string]: unknown;
11
- } & import("tslog").ILogObjMeta) | undefined;
12
- trace(...args: unknown[]): ({
11
+ } & import("tslog").ILogObjMeta);
12
+ trace(...args: unknown[]): void | ({
13
13
  [x: string]: unknown;
14
- } & import("tslog").ILogObjMeta) | undefined;
15
- debug(...args: unknown[]): ({
14
+ } & import("tslog").ILogObjMeta);
15
+ debug(...args: unknown[]): void | ({
16
16
  [x: string]: unknown;
17
- } & import("tslog").ILogObjMeta) | undefined;
18
- info(...args: unknown[]): ({
17
+ } & import("tslog").ILogObjMeta);
18
+ info(...args: unknown[]): void | ({
19
19
  [x: string]: unknown;
20
- } & import("tslog").ILogObjMeta) | undefined;
21
- warn(...args: unknown[]): ({
20
+ } & import("tslog").ILogObjMeta);
21
+ warn(...args: unknown[]): void | ({
22
22
  [x: string]: unknown;
23
- } & import("tslog").ILogObjMeta) | undefined;
24
- error(...args: unknown[]): ({
23
+ } & import("tslog").ILogObjMeta);
24
+ error(...args: unknown[]): void | ({
25
25
  [x: string]: unknown;
26
- } & import("tslog").ILogObjMeta) | undefined;
27
- fatal(...args: unknown[]): ({
26
+ } & import("tslog").ILogObjMeta);
27
+ fatal(...args: unknown[]): void | ({
28
28
  [x: string]: unknown;
29
- } & import("tslog").ILogObjMeta) | undefined;
29
+ } & import("tslog").ILogObjMeta);
30
30
  }
package/dist/types.d.ts CHANGED
@@ -4,10 +4,38 @@ import type { DependencyContainer, RegistrationOptions, InjectionToken as Tsyrin
4
4
  import type { ZodType } from "zod";
5
5
  export type Constructor<T = unknown> = new (...args: any[]) => T;
6
6
  export type ContextType = "http" | "rpc" | "ws";
7
- export type LoggerOptions = ISettingsParam<Record<string, unknown>>;
8
7
  export type InjectionToken<T = unknown> = TsyringeInjectionToken<T>;
9
8
  export type MetadataKey = string | symbol;
10
9
  export type ShutdownSignal = "SIGINT" | "SIGTERM";
10
+ export type LogLevel = "silly" | "trace" | "debug" | "info" | "warn" | "error" | "fatal";
11
+ export interface LogEntry {
12
+ level: LogLevel;
13
+ message: string;
14
+ args: unknown[];
15
+ timestamp: Date;
16
+ name?: string;
17
+ }
18
+ export interface LogTransport {
19
+ log(entry: LogEntry): void;
20
+ }
21
+ /** Native imperium logger options (transport-based) */
22
+ export interface ImperiumLoggerOptions {
23
+ name?: string;
24
+ minLevel?: LogLevel;
25
+ transports?: LogTransport[];
26
+ }
27
+ /** Legacy tslog options — still supported */
28
+ export type TslogOptions = ISettingsParam<Record<string, unknown>>;
29
+ /** Accepts either native imperium options or tslog settings */
30
+ export type LoggerOptions = ImperiumLoggerOptions | TslogOptions;
31
+ export type ErrorContextType = "http" | "rpc" | "ws" | "cron" | "events";
32
+ export interface ErrorContext {
33
+ type: ErrorContextType;
34
+ handler?: string;
35
+ controller?: string;
36
+ [key: string]: unknown;
37
+ }
38
+ export type OnErrorCallback = (error: unknown, context: ErrorContext) => void | Promise<void>;
11
39
  export interface ClassProvider<T = unknown> {
12
40
  provide: InjectionToken<T>;
13
41
  useClass: Constructor<T>;
@@ -172,6 +200,7 @@ export interface ServerOptions {
172
200
  health?: boolean | HealthOptions;
173
201
  gracefulShutdown?: boolean | GracefulShutdownOptions;
174
202
  loggerOptions?: LoggerOptions;
203
+ onError?: OnErrorCallback;
175
204
  config?: ConfigServiceOptions;
176
205
  }
177
206
  export {};
@@ -14,13 +14,13 @@ export declare const appConfigSchema: z.ZodObject<{
14
14
  CORS_EXPOSE_HEADERS: z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
15
15
  LOG_NAME: z.ZodDefault<z.ZodString>;
16
16
  LOG_LEVEL: z.ZodDefault<z.ZodEnum<{
17
- error: "error";
17
+ silly: "silly";
18
18
  trace: "trace";
19
- fatal: "fatal";
20
- warn: "warn";
21
- info: "info";
22
19
  debug: "debug";
23
- silly: "silly";
20
+ info: "info";
21
+ warn: "warn";
22
+ error: "error";
23
+ fatal: "fatal";
24
24
  }>>;
25
25
  LOG_TYPE: z.ZodDefault<z.ZodEnum<{
26
26
  pretty: "pretty";
@@ -67,19 +67,20 @@ function buildWsContext(gateway, methodName, handler, socket, request, message)
67
67
  function logWsError(app, scope, details, error) {
68
68
  try {
69
69
  scope.resolve(LoggerService).error(details, error);
70
- return;
71
70
  }
72
71
  catch {
73
- // fallback
74
- }
75
- try {
76
- app.getLogger().error(details, error);
77
- return;
78
- }
79
- catch {
80
- // final fallback
72
+ try {
73
+ app.getLogger().error(details, error);
74
+ }
75
+ catch {
76
+ console.error("[imperium] ws_error", details, error);
77
+ }
81
78
  }
82
- console.error("[imperium] ws_error", details, error);
79
+ app.reportError(error, {
80
+ type: "ws",
81
+ handler: details.type,
82
+ controller: details.gateway,
83
+ });
83
84
  }
84
85
  export function handleWsConnection(app, gateway, socket, request) {
85
86
  const scope = app.createRequestScope(gateway);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smounters/imperium",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "NestJS-like modular DI container with unified HTTP + Connect RPC server for TypeScript",
5
5
  "keywords": [
6
6
  "di",