@macss/modular-api 0.1.0 → 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,132 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // core/logger/logger.ts
4
+ // LogLevel enum, ModularLogger interface, RequestScopedLogger.
5
+ // Mirror of logger.dart — RFC 5424, JSON to stdout, zero deps.
6
+ // ============================================================
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.RequestScopedLogger = exports.LogLevel = void 0;
9
+ /**
10
+ * RFC 5424 log levels in descending severity order.
11
+ *
12
+ * Filtering rule: if configured `logLevel = X`, only messages with
13
+ * `value <= X` are emitted. Higher values produce total silence.
14
+ */
15
+ var LogLevel;
16
+ (function (LogLevel) {
17
+ LogLevel[LogLevel["emergency"] = 0] = "emergency";
18
+ LogLevel[LogLevel["alert"] = 1] = "alert";
19
+ LogLevel[LogLevel["critical"] = 2] = "critical";
20
+ LogLevel[LogLevel["error"] = 3] = "error";
21
+ LogLevel[LogLevel["warning"] = 4] = "warning";
22
+ LogLevel[LogLevel["notice"] = 5] = "notice";
23
+ LogLevel[LogLevel["info"] = 6] = "info";
24
+ LogLevel[LogLevel["debug"] = 7] = "debug";
25
+ })(LogLevel || (exports.LogLevel = LogLevel = {}));
26
+ /** Human-readable name for each LogLevel value. */
27
+ const LEVEL_NAME = {
28
+ [LogLevel.emergency]: 'emergency',
29
+ [LogLevel.alert]: 'alert',
30
+ [LogLevel.critical]: 'critical',
31
+ [LogLevel.error]: 'error',
32
+ [LogLevel.warning]: 'warning',
33
+ [LogLevel.notice]: 'notice',
34
+ [LogLevel.info]: 'info',
35
+ [LogLevel.debug]: 'debug',
36
+ };
37
+ /**
38
+ * Per-request logger that carries `traceId` and respects `logLevel` filtering.
39
+ *
40
+ * Created by `loggingMiddleware` for each incoming HTTP request and injected
41
+ * into the UseCase via the `logger` property.
42
+ *
43
+ * Accepts an optional `writeFn` for output — defaults to `console.log`.
44
+ * In tests, pass a capturing function to inspect output without side-effects.
45
+ */
46
+ class RequestScopedLogger {
47
+ constructor(traceId, logLevel, serviceName, writeFn = (line) => process.stdout.write(line + '\n')) {
48
+ this.traceId = traceId;
49
+ this.logLevel = logLevel;
50
+ this.serviceName = serviceName;
51
+ this.writeFn = writeFn;
52
+ }
53
+ // ─── Public API (8 RFC 5424 levels) ──────────────────────────────
54
+ emergency(msg, fields) {
55
+ this.log(LogLevel.emergency, msg, { fields });
56
+ }
57
+ alert(msg, fields) {
58
+ this.log(LogLevel.alert, msg, { fields });
59
+ }
60
+ critical(msg, fields) {
61
+ this.log(LogLevel.critical, msg, { fields });
62
+ }
63
+ error(msg, fields) {
64
+ this.log(LogLevel.error, msg, { fields });
65
+ }
66
+ warning(msg, fields) {
67
+ this.log(LogLevel.warning, msg, { fields });
68
+ }
69
+ notice(msg, fields) {
70
+ this.log(LogLevel.notice, msg, { fields });
71
+ }
72
+ info(msg, fields) {
73
+ this.log(LogLevel.info, msg, { fields });
74
+ }
75
+ debug(msg, fields) {
76
+ this.log(LogLevel.debug, msg, { fields });
77
+ }
78
+ // ─── Framework-internal: request/response logging ────────────────
79
+ /** Emits a "request received" log at `info` level. */
80
+ logRequest(opts) {
81
+ this.log(LogLevel.info, 'request received', {
82
+ extra: { method: opts.method, route: opts.route },
83
+ });
84
+ }
85
+ /** Emits a "request completed" log at the level determined by `statusCode`. */
86
+ logResponse(opts) {
87
+ this.log(RequestScopedLogger.levelForStatus(opts.statusCode), 'request completed', {
88
+ extra: {
89
+ method: opts.method,
90
+ route: opts.route,
91
+ status: opts.statusCode,
92
+ duration_ms: opts.durationMs,
93
+ },
94
+ });
95
+ }
96
+ /** Emits an "unhandled exception" log at `error` level. No stack trace. */
97
+ logUnhandledException(opts) {
98
+ this.log(LogLevel.error, 'unhandled exception', {
99
+ extra: { route: opts.route, status: 500 },
100
+ });
101
+ }
102
+ // ─── Internal ────────────────────────────────────────────────────
103
+ log(level, msg, opts = {}) {
104
+ // Filtering: only emit if the message level <= configured logLevel.
105
+ if (level > this.logLevel)
106
+ return;
107
+ const entry = {
108
+ ts: Date.now() / 1000,
109
+ level: LEVEL_NAME[level],
110
+ severity: level,
111
+ msg,
112
+ service: this.serviceName,
113
+ trace_id: this.traceId,
114
+ };
115
+ if (opts.extra)
116
+ Object.assign(entry, opts.extra);
117
+ if (opts.fields)
118
+ entry['fields'] = opts.fields;
119
+ this.writeFn(JSON.stringify(entry));
120
+ }
121
+ /** Maps HTTP status code → RFC 5424 log level. */
122
+ static levelForStatus(status) {
123
+ if (status >= 500)
124
+ return LogLevel.error;
125
+ if (status >= 400)
126
+ return LogLevel.warning;
127
+ if (status >= 200 && status < 400)
128
+ return LogLevel.info;
129
+ return LogLevel.notice; // 1xx
130
+ }
131
+ }
132
+ exports.RequestScopedLogger = RequestScopedLogger;
@@ -0,0 +1,25 @@
1
+ import type { RequestHandler } from 'express';
2
+ import { LogLevel } from './logger';
3
+ import type { WriteFn } from './logger';
4
+ /** Key used in `res.locals` to propagate the logger to downstream handlers. */
5
+ export declare const LOGGER_LOCALS_KEY = "modularLogger";
6
+ export interface LoggingMiddlewareOptions {
7
+ logLevel: LogLevel;
8
+ serviceName: string;
9
+ excludedRoutes?: string[];
10
+ /** Override output for testing. Defaults to stdout. */
11
+ writeFn?: WriteFn;
12
+ }
13
+ /**
14
+ * Creates an Express middleware that:
15
+ *
16
+ * 1. Reads or generates a `trace_id` (from `X-Request-ID` header).
17
+ * 2. Creates a {@link RequestScopedLogger} scoped to the current request.
18
+ * 3. Emits a `"request received"` log at `info` level.
19
+ * 4. Passes the logger via `res.locals` for downstream handlers.
20
+ * 5. Emits a `"request completed"` log (level based on status code).
21
+ * 6. Returns the `X-Request-ID` header in the response.
22
+ *
23
+ * Requests whose path matches `excludedRoutes` are passed through silently.
24
+ */
25
+ export declare function loggingMiddleware(opts: LoggingMiddlewareOptions): RequestHandler;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // core/logger/logging_middleware.ts
4
+ // Express middleware — trace_id, structured JSON logs.
5
+ // Mirror of logging_middleware.dart.
6
+ // ============================================================
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.LOGGER_LOCALS_KEY = void 0;
9
+ exports.loggingMiddleware = loggingMiddleware;
10
+ const node_crypto_1 = require("node:crypto");
11
+ const logger_1 = require("./logger");
12
+ /** Key used in `res.locals` to propagate the logger to downstream handlers. */
13
+ exports.LOGGER_LOCALS_KEY = 'modularLogger';
14
+ /**
15
+ * Creates an Express middleware that:
16
+ *
17
+ * 1. Reads or generates a `trace_id` (from `X-Request-ID` header).
18
+ * 2. Creates a {@link RequestScopedLogger} scoped to the current request.
19
+ * 3. Emits a `"request received"` log at `info` level.
20
+ * 4. Passes the logger via `res.locals` for downstream handlers.
21
+ * 5. Emits a `"request completed"` log (level based on status code).
22
+ * 6. Returns the `X-Request-ID` header in the response.
23
+ *
24
+ * Requests whose path matches `excludedRoutes` are passed through silently.
25
+ */
26
+ function loggingMiddleware(opts) {
27
+ const excludedSet = new Set(opts.excludedRoutes ?? []);
28
+ return (req, res, next) => {
29
+ const path = req.path;
30
+ // Skip excluded routes (health, metrics, docs).
31
+ if (excludedSet.has(path)) {
32
+ return next();
33
+ }
34
+ // 1. Resolve trace_id
35
+ const headerValue = req.headers['x-request-id'];
36
+ const traceId = typeof headerValue === 'string' && headerValue.length > 0 ? headerValue : (0, node_crypto_1.randomUUID)();
37
+ // 2. Create per-request logger
38
+ const logger = new logger_1.RequestScopedLogger(traceId, opts.logLevel, opts.serviceName, opts.writeFn);
39
+ const method = req.method.toUpperCase();
40
+ const route = path;
41
+ // 3. "request received"
42
+ logger.logRequest({ method, route });
43
+ // 4. Propagate logger via res.locals
44
+ res.locals[exports.LOGGER_LOCALS_KEY] = logger;
45
+ // 5. Attach X-Request-ID to response
46
+ res.setHeader('X-Request-ID', traceId);
47
+ // 6. Capture timing and emit response log on finish
48
+ const startNs = process.hrtime.bigint();
49
+ res.on('finish', () => {
50
+ const durationMs = Number(process.hrtime.bigint() - startNs) / 1e6;
51
+ logger.logResponse({
52
+ method,
53
+ route,
54
+ statusCode: res.statusCode,
55
+ durationMs,
56
+ });
57
+ });
58
+ next();
59
+ };
60
+ }
@@ -0,0 +1,32 @@
1
+ import { Registry, Counter, Gauge, Histogram, type CounterConfiguration, type GaugeConfiguration, type HistogramConfiguration } from 'prom-client';
2
+ /**
3
+ * Internal registry wrapping prom-client's `Registry`.
4
+ *
5
+ * On construction, registers `process_start_time_seconds` as a gauge
6
+ * set to the current epoch in seconds.
7
+ */
8
+ export declare class MetricRegistry {
9
+ readonly registry: Registry;
10
+ private readonly names;
11
+ constructor();
12
+ createCounter<T extends string = string>(config: Pick<CounterConfiguration<T>, 'name' | 'help' | 'labelNames'>): Counter<T>;
13
+ createGauge<T extends string = string>(config: Pick<GaugeConfiguration<T>, 'name' | 'help' | 'labelNames'>): Gauge<T>;
14
+ createHistogram<T extends string = string>(config: Pick<HistogramConfiguration<T>, 'name' | 'help' | 'labelNames' | 'buckets'>): Histogram<T>;
15
+ /** Serializes all metrics to Prometheus text exposition format. */
16
+ serialize(): Promise<string>;
17
+ /** Content type for the Prometheus text format. */
18
+ get contentType(): string;
19
+ private assertUnique;
20
+ }
21
+ /**
22
+ * Public API for users to register custom metrics.
23
+ * Validates names and rejects reserved prefixes before delegating.
24
+ */
25
+ export declare class MetricsRegistrar {
26
+ private readonly registry;
27
+ constructor(registry: MetricRegistry);
28
+ createCounter<T extends string = string>(config: Pick<CounterConfiguration<T>, 'name' | 'help' | 'labelNames'>): Counter<T>;
29
+ createGauge<T extends string = string>(config: Pick<GaugeConfiguration<T>, 'name' | 'help' | 'labelNames'>): Gauge<T>;
30
+ createHistogram<T extends string = string>(config: Pick<HistogramConfiguration<T>, 'name' | 'help' | 'labelNames' | 'buckets'>): Histogram<T>;
31
+ private validate;
32
+ }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // core/metrics/metric_registry.ts
4
+ // Wraps prom-client with a two-layer API:
5
+ // MetricRegistry — internal, manages prom-client Registry
6
+ // MetricsRegistrar — public, validates names before delegating
7
+ // ============================================================
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.MetricsRegistrar = exports.MetricRegistry = void 0;
10
+ const prom_client_1 = require("prom-client");
11
+ /** Prometheus metric name regex. */
12
+ const VALID_NAME = /^[a-zA-Z_:][a-zA-Z0-9_:]*$/;
13
+ function assertValidName(name) {
14
+ if (!name || !VALID_NAME.test(name)) {
15
+ throw new Error(`Invalid metric name "${name}": must match [a-zA-Z_:][a-zA-Z0-9_:]*`);
16
+ }
17
+ }
18
+ // ── MetricRegistry (internal) ─────────────────────────────────────────
19
+ /**
20
+ * Internal registry wrapping prom-client's `Registry`.
21
+ *
22
+ * On construction, registers `process_start_time_seconds` as a gauge
23
+ * set to the current epoch in seconds.
24
+ */
25
+ class MetricRegistry {
26
+ constructor() {
27
+ this.names = new Set();
28
+ this.registry = new prom_client_1.Registry();
29
+ // Auto-register process_start_time_seconds.
30
+ const startTime = this.createGauge({
31
+ name: 'process_start_time_seconds',
32
+ help: 'Start time of the process since unix epoch in seconds.',
33
+ });
34
+ startTime.set(Date.now() / 1000);
35
+ }
36
+ createCounter(config) {
37
+ this.assertUnique(config.name);
38
+ const counter = new prom_client_1.Counter({
39
+ ...config,
40
+ registers: [this.registry],
41
+ });
42
+ return counter;
43
+ }
44
+ createGauge(config) {
45
+ this.assertUnique(config.name);
46
+ const gauge = new prom_client_1.Gauge({
47
+ ...config,
48
+ registers: [this.registry],
49
+ });
50
+ return gauge;
51
+ }
52
+ createHistogram(config) {
53
+ this.assertUnique(config.name);
54
+ const histogram = new prom_client_1.Histogram({
55
+ ...config,
56
+ registers: [this.registry],
57
+ });
58
+ return histogram;
59
+ }
60
+ /** Serializes all metrics to Prometheus text exposition format. */
61
+ async serialize() {
62
+ return this.registry.metrics();
63
+ }
64
+ /** Content type for the Prometheus text format. */
65
+ get contentType() {
66
+ return this.registry.contentType;
67
+ }
68
+ assertUnique(name) {
69
+ if (this.names.has(name)) {
70
+ throw new Error(`Metric "${name}" is already registered.`);
71
+ }
72
+ this.names.add(name);
73
+ }
74
+ }
75
+ exports.MetricRegistry = MetricRegistry;
76
+ // ── MetricsRegistrar (public) ─────────────────────────────────────────
77
+ /**
78
+ * Public API for users to register custom metrics.
79
+ * Validates names and rejects reserved prefixes before delegating.
80
+ */
81
+ class MetricsRegistrar {
82
+ constructor(registry) {
83
+ this.registry = registry;
84
+ }
85
+ createCounter(config) {
86
+ this.validate(config.name);
87
+ return this.registry.createCounter(config);
88
+ }
89
+ createGauge(config) {
90
+ this.validate(config.name);
91
+ return this.registry.createGauge(config);
92
+ }
93
+ createHistogram(config) {
94
+ this.validate(config.name);
95
+ return this.registry.createHistogram(config);
96
+ }
97
+ validate(name) {
98
+ assertValidName(name);
99
+ if (name.startsWith('__')) {
100
+ throw new Error(`Metric name "${name}" uses reserved prefix "__".`);
101
+ }
102
+ }
103
+ }
104
+ exports.MetricsRegistrar = MetricsRegistrar;
@@ -0,0 +1,24 @@
1
+ import type { RequestHandler } from 'express';
2
+ import type { Counter, Gauge, Histogram } from 'prom-client';
3
+ import type { MetricRegistry } from './metric_registry';
4
+ export interface MetricsMiddlewareOptions {
5
+ requestsTotal: Counter<'method' | 'route' | 'status_code'>;
6
+ requestsInFlight: Gauge;
7
+ requestDuration: Histogram<'method' | 'route' | 'status_code'>;
8
+ excludedRoutes: string[];
9
+ registeredPaths: string[];
10
+ }
11
+ /**
12
+ * Creates an Express middleware that instruments HTTP requests.
13
+ *
14
+ * Records:
15
+ * - `requestsTotal` — counter with labels: method, route, status_code
16
+ * - `requestsInFlight` — gauge (inc on entry, dec on finish)
17
+ * - `requestDuration` — histogram with labels: method, route, status_code
18
+ */
19
+ export declare function metricsMiddleware(opts: MetricsMiddlewareOptions): RequestHandler;
20
+ /**
21
+ * Creates an Express handler that returns Prometheus metrics.
22
+ * Always returns HTTP 200 with `text/plain; version=0.0.4; charset=utf-8`.
23
+ */
24
+ export declare function metricsHandler(registry: MetricRegistry): RequestHandler;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // core/metrics/metrics_middleware.ts
4
+ // Express middleware + handler for Prometheus metrics.
5
+ // ============================================================
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.metricsMiddleware = metricsMiddleware;
8
+ exports.metricsHandler = metricsHandler;
9
+ /**
10
+ * Creates an Express middleware that instruments HTTP requests.
11
+ *
12
+ * Records:
13
+ * - `requestsTotal` — counter with labels: method, route, status_code
14
+ * - `requestsInFlight` — gauge (inc on entry, dec on finish)
15
+ * - `requestDuration` — histogram with labels: method, route, status_code
16
+ */
17
+ function metricsMiddleware(opts) {
18
+ const excludedSet = new Set(opts.excludedRoutes);
19
+ const registeredSet = new Set(opts.registeredPaths);
20
+ return (req, res, next) => {
21
+ const path = req.path;
22
+ // Skip excluded routes.
23
+ if (excludedSet.has(path)) {
24
+ return next();
25
+ }
26
+ const method = req.method.toUpperCase();
27
+ const route = registeredSet.has(path) ? path : 'UNMATCHED';
28
+ opts.requestsInFlight.inc();
29
+ const startTime = process.hrtime.bigint();
30
+ res.on('finish', () => {
31
+ opts.requestsInFlight.dec();
32
+ const durationNs = Number(process.hrtime.bigint() - startTime);
33
+ const durationSecs = durationNs / 1e9;
34
+ const statusCode = res.statusCode.toString();
35
+ const labels = { method, route, status_code: statusCode };
36
+ opts.requestsTotal.inc(labels);
37
+ opts.requestDuration.observe(labels, durationSecs);
38
+ });
39
+ next();
40
+ };
41
+ }
42
+ /**
43
+ * Creates an Express handler that returns Prometheus metrics.
44
+ * Always returns HTTP 200 with `text/plain; version=0.0.4; charset=utf-8`.
45
+ */
46
+ function metricsHandler(registry) {
47
+ return async (_req, res) => {
48
+ const body = await registry.serialize();
49
+ res.status(200).set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8').send(body);
50
+ };
51
+ }
@@ -1,10 +1,31 @@
1
1
  import { type RequestHandler } from 'express';
2
2
  import { ModuleBuilder } from './module_builder';
3
+ import type { HealthCheck } from './health/health_check';
4
+ import { MetricsRegistrar } from './metrics/metric_registry';
5
+ import { LogLevel } from './logger/logger';
3
6
  export interface ModularApiOptions {
4
7
  /** Base path prefix for all module routes. Default: '/api' */
5
8
  basePath?: string;
6
9
  /** API title shown in Swagger UI. Default: 'API' */
7
10
  title?: string;
11
+ /** API version string (e.g. '1.0.0'). Used in health check response. Default: '0.0.0' */
12
+ version?: string;
13
+ /**
14
+ * Release identifier. Defaults to `version-debug`.
15
+ * Override via `process.env.RELEASE_ID`.
16
+ */
17
+ releaseId?: string;
18
+ /** Opt-in Prometheus metrics endpoint. Default: false */
19
+ metricsEnabled?: boolean;
20
+ /** Path for the metrics endpoint. Default: '/metrics' */
21
+ metricsPath?: string;
22
+ /** Routes excluded from instrumentation. Default: ['/metrics', '/health', '/docs'] */
23
+ excludedMetricsRoutes?: string[];
24
+ /**
25
+ * Minimum log level for the structured JSON logger.
26
+ * Default: LogLevel.info (emits emergency..info, suppresses debug).
27
+ */
28
+ logLevel?: LogLevel;
8
29
  }
9
30
  /**
10
31
  * Main entry point for modular_api.
@@ -24,7 +45,7 @@ export interface ModularApiOptions {
24
45
  * ```
25
46
  *
26
47
  * Auto-mounted endpoints:
27
- * GET /health → 200 "ok"
48
+ * GET /health → 200/503 application/health+json (IETF draft)
28
49
  * GET /docs → Swagger UI
29
50
  */
30
51
  export declare class ModularApi {
@@ -33,7 +54,28 @@ export declare class ModularApi {
33
54
  private readonly basePath;
34
55
  private readonly title;
35
56
  private readonly middlewares;
57
+ private readonly healthService;
58
+ private readonly metricsEnabled;
59
+ private readonly metricsPath;
60
+ private readonly excludedMetricsRoutes;
61
+ private readonly metricRegistry?;
62
+ private readonly _metricsRegistrar?;
63
+ private readonly httpRequestsTotal?;
64
+ private readonly httpRequestsInFlight?;
65
+ private readonly httpRequestDuration?;
66
+ private readonly logLevel;
67
+ /** Public accessor for custom-metric registration. Undefined when metrics are disabled. */
68
+ get metrics(): MetricsRegistrar | undefined;
36
69
  constructor(options?: ModularApiOptions);
70
+ /**
71
+ * Register a {@link HealthCheck} to be evaluated on `GET /health`.
72
+ * Returns `this` for method chaining.
73
+ *
74
+ * ```ts
75
+ * api.addHealthCheck(new DatabaseHealthCheck());
76
+ * ```
77
+ */
78
+ addHealthCheck(check: HealthCheck): this;
37
79
  /**
38
80
  * Registers a group of use cases under a named module.
39
81
  * Returns `this` for method chaining.
@@ -13,6 +13,13 @@ const express_1 = __importDefault(require("express"));
13
13
  const module_builder_1 = require("./module_builder");
14
14
  const openapi_1 = require("../openapi/openapi");
15
15
  const swagger_ui_express_1 = __importDefault(require("swagger-ui-express"));
16
+ const health_service_1 = require("./health/health_service");
17
+ const health_handler_1 = require("./health/health_handler");
18
+ const metric_registry_1 = require("./metrics/metric_registry");
19
+ const metrics_middleware_1 = require("./metrics/metrics_middleware");
20
+ const logging_middleware_1 = require("./logger/logging_middleware");
21
+ const logger_1 = require("./logger/logger");
22
+ const registry_1 = require("./registry");
16
23
  /**
17
24
  * Main entry point for modular_api.
18
25
  *
@@ -31,18 +38,61 @@ const swagger_ui_express_1 = __importDefault(require("swagger-ui-express"));
31
38
  * ```
32
39
  *
33
40
  * Auto-mounted endpoints:
34
- * GET /health → 200 "ok"
41
+ * GET /health → 200/503 application/health+json (IETF draft)
35
42
  * GET /docs → Swagger UI
36
43
  */
37
44
  class ModularApi {
45
+ /** Public accessor for custom-metric registration. Undefined when metrics are disabled. */
46
+ get metrics() {
47
+ return this._metricsRegistrar;
48
+ }
38
49
  constructor(options = {}) {
39
50
  this.middlewares = [];
40
51
  this.basePath = options.basePath ?? '/api';
41
- this.title = options.title ?? 'API';
52
+ this.title = options.title ?? 'Modular API';
53
+ this.healthService = new health_service_1.HealthService({
54
+ version: options.version ?? 'x.y.z',
55
+ releaseId: options.releaseId,
56
+ });
57
+ // Metrics setup
58
+ this.metricsEnabled = options.metricsEnabled ?? false;
59
+ this.metricsPath = options.metricsPath ?? '/metrics';
60
+ this.excludedMetricsRoutes = options.excludedMetricsRoutes ?? ['/metrics', '/health', '/docs'];
61
+ // Logging
62
+ this.logLevel = options.logLevel ?? logger_1.LogLevel.info;
63
+ if (this.metricsEnabled) {
64
+ this.metricRegistry = new metric_registry_1.MetricRegistry();
65
+ this._metricsRegistrar = new metric_registry_1.MetricsRegistrar(this.metricRegistry);
66
+ this.httpRequestsTotal = this.metricRegistry.createCounter({
67
+ name: 'http_requests_total',
68
+ help: 'Total number of HTTP requests.',
69
+ labelNames: ['method', 'route', 'status_code'],
70
+ });
71
+ this.httpRequestsInFlight = this.metricRegistry.createGauge({
72
+ name: 'http_requests_in_flight',
73
+ help: 'Number of HTTP requests currently being processed.',
74
+ });
75
+ this.httpRequestDuration = this.metricRegistry.createHistogram({
76
+ name: 'http_request_duration_seconds',
77
+ help: 'HTTP request duration in seconds.',
78
+ labelNames: ['method', 'route', 'status_code'],
79
+ });
80
+ }
42
81
  this.app = (0, express_1.default)();
43
82
  this.app.use(express_1.default.json());
44
83
  this.rootRouter = express_1.default.Router();
45
- this.app.use(this.rootRouter);
84
+ }
85
+ /**
86
+ * Register a {@link HealthCheck} to be evaluated on `GET /health`.
87
+ * Returns `this` for method chaining.
88
+ *
89
+ * ```ts
90
+ * api.addHealthCheck(new DatabaseHealthCheck());
91
+ * ```
92
+ */
93
+ addHealthCheck(check) {
94
+ this.healthService.addHealthCheck(check);
95
+ return this;
46
96
  }
47
97
  /**
48
98
  * Registers a group of use cases under a named module.
@@ -88,18 +138,49 @@ class ModularApi {
88
138
  serve(options) {
89
139
  const { port, host = '0.0.0.0' } = options;
90
140
  return new Promise((resolve) => {
141
+ // Logging middleware FIRST — trace_id + structured JSON logs.
142
+ const excludedLogRoutes = ['/health', this.metricsPath, '/docs', '/docs/'];
143
+ this.app.use((0, logging_middleware_1.loggingMiddleware)({
144
+ logLevel: this.logLevel,
145
+ serviceName: this.title,
146
+ excludedRoutes: excludedLogRoutes,
147
+ }));
148
+ // Metrics middleware — before user middlewares & routes.
149
+ // Created here so registeredPaths is populated from apiRegistry.
150
+ if (this.metricsEnabled &&
151
+ this.httpRequestsTotal &&
152
+ this.httpRequestsInFlight &&
153
+ this.httpRequestDuration) {
154
+ const registeredPaths = registry_1.apiRegistry.routes.map((r) => r.path);
155
+ this.app.use((0, metrics_middleware_1.metricsMiddleware)({
156
+ requestsTotal: this.httpRequestsTotal,
157
+ requestsInFlight: this.httpRequestsInFlight,
158
+ requestDuration: this.httpRequestDuration,
159
+ excludedRoutes: this.excludedMetricsRoutes,
160
+ registeredPaths,
161
+ }));
162
+ }
91
163
  // Register middlewares before routes
92
164
  for (const mw of this.middlewares) {
93
165
  this.app.use(mw);
94
166
  }
95
- // Health endpoint
96
- this.app.get('/health', (_req, res) => res.status(200).send('ok'));
167
+ // Metrics endpoint (before rootRouter — its own handler).
168
+ if (this.metricsEnabled && this.metricRegistry) {
169
+ this.app.get(this.metricsPath, (0, metrics_middleware_1.metricsHandler)(this.metricRegistry));
170
+ }
171
+ // Health endpoint — IETF Health Check Response Format
172
+ this.app.get('/health', (0, health_handler_1.healthHandler)(this.healthService));
173
+ // Module use case routes.
174
+ this.app.use(this.rootRouter);
97
175
  // Swagger / OpenAPI docs
98
176
  const spec = (0, openapi_1.buildOpenApiSpec)({ title: this.title, port });
99
177
  this.app.use('/docs', swagger_ui_express_1.default.serve, swagger_ui_express_1.default.setup(spec));
100
178
  const server = this.app.listen(port, host, () => {
101
179
  console.log(`Docs → http://localhost:${port}/docs`);
102
180
  console.log(`Health → http://localhost:${port}/health`);
181
+ if (this.metricsEnabled) {
182
+ console.log(`Metrics → http://localhost:${port}${this.metricsPath}`);
183
+ }
103
184
  resolve(server);
104
185
  });
105
186
  });
@@ -1,3 +1,4 @@
1
+ import type { ModularLogger } from './logger/logger';
1
2
  /**
2
3
  * **Contract** — use `implements Input`.
3
4
  *
@@ -94,6 +95,12 @@ export declare abstract class UseCase<I extends Input, O extends Output> {
94
95
  abstract readonly input: I;
95
96
  /** Output DTO — set in execute(). */
96
97
  abstract output: O;
98
+ /**
99
+ * Request-scoped logger injected by the framework's logging middleware.
100
+ * Available inside `execute()`. Undefined when running without middleware
101
+ * or in tests that don't provide one.
102
+ */
103
+ logger?: ModularLogger;
97
104
  /**
98
105
  * Synchronous validation.
99
106
  * Return a human-readable error string to abort execution with HTTP 400.
@@ -7,6 +7,7 @@
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.useCaseHandler = useCaseHandler;
9
9
  const use_case_exception_1 = require("./use_case_exception");
10
+ const logging_middleware_1 = require("./logger/logging_middleware");
10
11
  const JSON_HEADERS = { 'Content-Type': 'application/json; charset=utf-8' };
11
12
  /**
12
13
  * Wraps any UseCase factory into an Express RequestHandler.
@@ -33,9 +34,14 @@ function useCaseHandler(factory) {
33
34
  // 1. Extract payload
34
35
  const data = req.method.toUpperCase() === 'GET' || req.method.toUpperCase() === 'DELETE'
35
36
  ? { ...req.query, ...req.params }
36
- : req.body ?? {};
37
+ : (req.body ?? {});
37
38
  // 2. Build use case
38
39
  const useCase = factory(data);
40
+ // 2b. Inject request-scoped logger (if logging middleware is active)
41
+ const logger = res.locals[logging_middleware_1.LOGGER_LOCALS_KEY];
42
+ if (logger) {
43
+ useCase.logger = logger;
44
+ }
39
45
  // 3. Validate
40
46
  const validationError = useCase.validate();
41
47
  if (validationError !== null) {