@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.
- package/README.md +122 -131
- package/dist/core/health/health_check.d.ts +50 -0
- package/dist/core/health/health_check.js +69 -0
- package/dist/core/health/health_handler.d.ts +9 -0
- package/dist/core/health/health_handler.js +23 -0
- package/dist/core/health/health_service.d.ts +60 -0
- package/dist/core/health/health_service.js +132 -0
- package/dist/core/logger/logger.d.ts +79 -0
- package/dist/core/logger/logger.js +132 -0
- package/dist/core/logger/logging_middleware.d.ts +25 -0
- package/dist/core/logger/logging_middleware.js +60 -0
- package/dist/core/metrics/metric_registry.d.ts +32 -0
- package/dist/core/metrics/metric_registry.js +104 -0
- package/dist/core/metrics/metrics_middleware.d.ts +24 -0
- package/dist/core/metrics/metrics_middleware.js +51 -0
- package/dist/core/modular_api.d.ts +43 -1
- package/dist/core/modular_api.js +86 -5
- package/dist/core/usecase.d.ts +7 -0
- package/dist/core/usecase_handler.js +7 -1
- package/dist/core/usecase_test_handler.d.ts +4 -1
- package/dist/core/usecase_test_handler.js +5 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +24 -1
- package/dist/openapi/openapi.js +1 -3
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,131 +1,122 @@
|
|
|
1
|
-
# modular_api
|
|
2
|
-
|
|
3
|
-
Use-case centric toolkit for building modular APIs with Express.
|
|
4
|
-
Define `UseCase` classes (input → validate → execute → output), connect them to HTTP routes, and get automatic Swagger/OpenAPI documentation.
|
|
5
|
-
|
|
6
|
-
>
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## Quick start
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
import { ModularApi, ModuleBuilder } from 'modular_api';
|
|
14
|
-
|
|
15
|
-
// ─── Module builder (separate file in real projects) ──────────
|
|
16
|
-
function buildGreetingsModule(m: ModuleBuilder): void {
|
|
17
|
-
m.usecase('hello', HelloWorld.fromJson);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// ─── Server ───────────────────────────────────────────────────
|
|
21
|
-
const api = new ModularApi({ basePath: '/api' });
|
|
22
|
-
|
|
23
|
-
api.module('greetings', buildGreetingsModule);
|
|
24
|
-
|
|
25
|
-
api.serve({ port: 8080 });
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
curl -X POST http://localhost:8080/api/greetings/hello \
|
|
30
|
-
-H "Content-Type: application/json" \
|
|
31
|
-
-d '{"name":"World"}'
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
```json
|
|
35
|
-
{"message":"Hello, World!"}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
**Docs** → `http://localhost:8080/docs`
|
|
39
|
-
**Health** → `http://localhost:8080/health`
|
|
40
|
-
|
|
41
|
-
See `example/example.ts` for the full implementation including Input, Output, UseCase with `validate()`, and the builder.
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## Features
|
|
46
|
-
|
|
47
|
-
- `UseCase<I, O>` — pure business logic, no HTTP concerns
|
|
48
|
-
- `Input` / `Output` — DTOs with `toJson()` and `toSchema()` for automatic OpenAPI
|
|
49
|
-
- `Output.statusCode` — custom HTTP status codes per response
|
|
50
|
-
- `UseCaseException` — structured error handling (status code, message, error code, details)
|
|
51
|
-
- `ModularApi` + `ModuleBuilder` — module registration and routing
|
|
52
|
-
- `useCaseTestHandler` — unit test helper (no HTTP server needed)
|
|
53
|
-
- `cors()` middleware — built-in CORS support
|
|
54
|
-
- Swagger UI at `/docs` — auto-generated from registered use cases
|
|
55
|
-
- Health check at `GET /health`
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
- **
|
|
114
|
-
- **
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
- **GitHub**: [macss-dev/modular_api](https://github.com/macss-dev/modular_api)
|
|
124
|
-
|
|
125
|
-
Both SDKs share the same architecture and API surface at v0.1.0.
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## License
|
|
130
|
-
|
|
131
|
-
MIT © [ccisne.dev](https://ccisne.dev)
|
|
1
|
+
# modular_api
|
|
2
|
+
|
|
3
|
+
Use-case centric toolkit for building modular APIs with Express.
|
|
4
|
+
Define `UseCase` classes (input → validate → execute → output), connect them to HTTP routes, and get automatic Swagger/OpenAPI documentation.
|
|
5
|
+
|
|
6
|
+
> Also available in **Dart**: [modular_api](https://pub.dev/packages/modular_api)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { ModularApi, ModuleBuilder } from 'modular_api';
|
|
14
|
+
|
|
15
|
+
// ─── Module builder (separate file in real projects) ──────────
|
|
16
|
+
function buildGreetingsModule(m: ModuleBuilder): void {
|
|
17
|
+
m.usecase('hello', HelloWorld.fromJson);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Server ───────────────────────────────────────────────────
|
|
21
|
+
const api = new ModularApi({ basePath: '/api' });
|
|
22
|
+
|
|
23
|
+
api.module('greetings', buildGreetingsModule);
|
|
24
|
+
|
|
25
|
+
api.serve({ port: 8080 });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
curl -X POST http://localhost:8080/api/greetings/hello \
|
|
30
|
+
-H "Content-Type: application/json" \
|
|
31
|
+
-d '{"name":"World"}'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{ "message": "Hello, World!" }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Docs** → `http://localhost:8080/docs`
|
|
39
|
+
**Health** → `http://localhost:8080/health`
|
|
40
|
+
|
|
41
|
+
See `example/example.ts` for the full implementation including Input, Output, UseCase with `validate()`, and the builder.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- `UseCase<I, O>` — pure business logic, no HTTP concerns
|
|
48
|
+
- `Input` / `Output` — DTOs with `toJson()` and `toSchema()` for automatic OpenAPI
|
|
49
|
+
- `Output.statusCode` — custom HTTP status codes per response
|
|
50
|
+
- `UseCaseException` — structured error handling (status code, message, error code, details)
|
|
51
|
+
- `ModularApi` + `ModuleBuilder` — module registration and routing
|
|
52
|
+
- `useCaseTestHandler` — unit test helper (no HTTP server needed)
|
|
53
|
+
- `cors()` middleware — built-in CORS support
|
|
54
|
+
- Swagger UI at `/docs` — auto-generated from registered use cases
|
|
55
|
+
- Health check at `GET /health` — [IETF Health Check Response Format](doc/health_check_guide.md)
|
|
56
|
+
- Prometheus metrics at `GET /metrics` — [Prometheus exposition format](doc/metrics_guide.md)
|
|
57
|
+
- Structured JSON logging — Loki/Grafana compatible, [request-scoped with trace_id](doc/logger_guide.md)
|
|
58
|
+
- All endpoints default to `POST` (configurable per use case)
|
|
59
|
+
- Full TypeScript declarations (`.d.ts`) included
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install @macss/modular-api
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Error handling
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
async execute() {
|
|
75
|
+
const user = await repository.findById(this.input.userId);
|
|
76
|
+
if (!user) {
|
|
77
|
+
throw new UseCaseException({
|
|
78
|
+
statusCode: 404,
|
|
79
|
+
message: 'User not found',
|
|
80
|
+
errorCode: 'USER_NOT_FOUND',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
this.output = new GetUserOutput(user);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{ "error": "USER_NOT_FOUND", "message": "User not found" }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Testing
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { useCaseTestHandler } from 'modular_api';
|
|
97
|
+
|
|
98
|
+
const handler = useCaseTestHandler(HelloWorld.fromJson);
|
|
99
|
+
const response = await handler({ name: 'World' });
|
|
100
|
+
|
|
101
|
+
console.log(response.statusCode); // 200
|
|
102
|
+
console.log(response.body); // { message: 'Hello, World!' }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Architecture
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
HTTP Request → ModularApi → Module → UseCase → Business Logic → Output → HTTP Response
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- **UseCase layer** — pure logic, independent of HTTP
|
|
114
|
+
- **HTTP adapter** — turns a UseCase into an Express RequestHandler
|
|
115
|
+
- **Middlewares** — cross-cutting concerns (CORS, logging)
|
|
116
|
+
- **Swagger UI** — documentation served automatically
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT © [ccisne.dev](https://ccisne.dev)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health status values.
|
|
3
|
+
*
|
|
4
|
+
* - `'pass'` — The component is healthy.
|
|
5
|
+
* - `'warn'` — The component is healthy but has a warning condition.
|
|
6
|
+
* - `'fail'` — The component is unhealthy.
|
|
7
|
+
*/
|
|
8
|
+
export type HealthStatus = 'pass' | 'warn' | 'fail';
|
|
9
|
+
/** Compare two statuses and return the worse one. */
|
|
10
|
+
export declare function worstStatus(a: HealthStatus, b: HealthStatus): HealthStatus;
|
|
11
|
+
/**
|
|
12
|
+
* Result returned by a single {@link HealthCheck}.
|
|
13
|
+
*/
|
|
14
|
+
export declare class HealthCheckResult {
|
|
15
|
+
readonly status: HealthStatus;
|
|
16
|
+
readonly responseTime?: number;
|
|
17
|
+
readonly output?: string;
|
|
18
|
+
constructor(status: HealthStatus, options?: {
|
|
19
|
+
responseTime?: number;
|
|
20
|
+
output?: string;
|
|
21
|
+
});
|
|
22
|
+
/** Return a copy with `responseTime` set. */
|
|
23
|
+
withResponseTime(ms: number): HealthCheckResult;
|
|
24
|
+
/** Serialize to the IETF JSON structure. Only includes optional fields when defined. */
|
|
25
|
+
toJson(): Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Abstract base for custom health checks.
|
|
29
|
+
*
|
|
30
|
+
* Implementors must provide `name` and `check()`.
|
|
31
|
+
* Override `timeout` to change the default 5 000 ms deadline.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* class DatabaseHealthCheck extends HealthCheck {
|
|
35
|
+
* readonly name = 'database';
|
|
36
|
+
* async check() {
|
|
37
|
+
* await db.ping();
|
|
38
|
+
* return new HealthCheckResult('pass');
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare abstract class HealthCheck {
|
|
44
|
+
/** Display name used as the key in the `checks` map. */
|
|
45
|
+
abstract readonly name: string;
|
|
46
|
+
/** Maximum time (ms) before the check is considered failed. Default: 5 000. */
|
|
47
|
+
get timeout(): number;
|
|
48
|
+
/** Execute the health check and return a result. */
|
|
49
|
+
abstract check(): Promise<HealthCheckResult>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// core/health/health_check.ts
|
|
4
|
+
// Health check types — IETF Health Check Response Format draft.
|
|
5
|
+
// Mirror of health_check.dart in Dart.
|
|
6
|
+
// ============================================================
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.HealthCheck = exports.HealthCheckResult = void 0;
|
|
9
|
+
exports.worstStatus = worstStatus;
|
|
10
|
+
/** Severity order for worst-status-wins aggregation. */
|
|
11
|
+
const SEVERITY = {
|
|
12
|
+
pass: 0,
|
|
13
|
+
warn: 1,
|
|
14
|
+
fail: 2,
|
|
15
|
+
};
|
|
16
|
+
/** Compare two statuses and return the worse one. */
|
|
17
|
+
function worstStatus(a, b) {
|
|
18
|
+
return SEVERITY[a] >= SEVERITY[b] ? a : b;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Result returned by a single {@link HealthCheck}.
|
|
22
|
+
*/
|
|
23
|
+
class HealthCheckResult {
|
|
24
|
+
constructor(status, options) {
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.responseTime = options?.responseTime;
|
|
27
|
+
this.output = options?.output;
|
|
28
|
+
}
|
|
29
|
+
/** Return a copy with `responseTime` set. */
|
|
30
|
+
withResponseTime(ms) {
|
|
31
|
+
return new HealthCheckResult(this.status, {
|
|
32
|
+
responseTime: ms,
|
|
33
|
+
output: this.output,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Serialize to the IETF JSON structure. Only includes optional fields when defined. */
|
|
37
|
+
toJson() {
|
|
38
|
+
const json = { status: this.status };
|
|
39
|
+
if (this.responseTime !== undefined)
|
|
40
|
+
json.responseTime = this.responseTime;
|
|
41
|
+
if (this.output !== undefined)
|
|
42
|
+
json.output = this.output;
|
|
43
|
+
return json;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.HealthCheckResult = HealthCheckResult;
|
|
47
|
+
/**
|
|
48
|
+
* Abstract base for custom health checks.
|
|
49
|
+
*
|
|
50
|
+
* Implementors must provide `name` and `check()`.
|
|
51
|
+
* Override `timeout` to change the default 5 000 ms deadline.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* class DatabaseHealthCheck extends HealthCheck {
|
|
55
|
+
* readonly name = 'database';
|
|
56
|
+
* async check() {
|
|
57
|
+
* await db.ping();
|
|
58
|
+
* return new HealthCheckResult('pass');
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
class HealthCheck {
|
|
64
|
+
/** Maximum time (ms) before the check is considered failed. Default: 5 000. */
|
|
65
|
+
get timeout() {
|
|
66
|
+
return 5000;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.HealthCheck = HealthCheck;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RequestHandler } from 'express';
|
|
2
|
+
import type { HealthService } from './health_service';
|
|
3
|
+
/**
|
|
4
|
+
* Creates an Express request handler that responds to `GET /health`
|
|
5
|
+
* with `application/health+json` following the IETF draft.
|
|
6
|
+
*
|
|
7
|
+
* Returns 200 for pass/warn, 503 for fail.
|
|
8
|
+
*/
|
|
9
|
+
export declare function healthHandler(service: HealthService): RequestHandler;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// core/health/health_handler.ts
|
|
4
|
+
// Express handler for GET /health — application/health+json.
|
|
5
|
+
// Mirror of health_handler.dart in Dart.
|
|
6
|
+
// ============================================================
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.healthHandler = healthHandler;
|
|
9
|
+
/**
|
|
10
|
+
* Creates an Express request handler that responds to `GET /health`
|
|
11
|
+
* with `application/health+json` following the IETF draft.
|
|
12
|
+
*
|
|
13
|
+
* Returns 200 for pass/warn, 503 for fail.
|
|
14
|
+
*/
|
|
15
|
+
function healthHandler(service) {
|
|
16
|
+
return async (_req, res) => {
|
|
17
|
+
const response = await service.evaluate();
|
|
18
|
+
res
|
|
19
|
+
.status(response.httpStatusCode)
|
|
20
|
+
.set('Content-Type', 'application/health+json')
|
|
21
|
+
.json(response.toJson());
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type HealthStatus, HealthCheck, HealthCheckResult } from './health_check';
|
|
2
|
+
/**
|
|
3
|
+
* Aggregated health response following the IETF Health Check Response Format.
|
|
4
|
+
*
|
|
5
|
+
* `httpStatusCode`: 200 for pass/warn, 503 for fail.
|
|
6
|
+
*/
|
|
7
|
+
export declare class HealthResponse {
|
|
8
|
+
readonly status: HealthStatus;
|
|
9
|
+
readonly version: string;
|
|
10
|
+
readonly releaseId: string;
|
|
11
|
+
readonly checks: Record<string, HealthCheckResult>;
|
|
12
|
+
constructor(options: {
|
|
13
|
+
status: HealthStatus;
|
|
14
|
+
version: string;
|
|
15
|
+
releaseId: string;
|
|
16
|
+
checks: Record<string, HealthCheckResult>;
|
|
17
|
+
});
|
|
18
|
+
/** HTTP status code: 200 for pass/warn, 503 for fail. */
|
|
19
|
+
get httpStatusCode(): number;
|
|
20
|
+
/** Serialize to the IETF-compliant JSON structure. */
|
|
21
|
+
toJson(): Record<string, any>;
|
|
22
|
+
}
|
|
23
|
+
export interface HealthServiceOptions {
|
|
24
|
+
/** API version string (e.g. '1.0.0'). */
|
|
25
|
+
version: string;
|
|
26
|
+
/**
|
|
27
|
+
* Release identifier. Defaults to `version-debug`.
|
|
28
|
+
* Override via `process.env.RELEASE_ID`.
|
|
29
|
+
*/
|
|
30
|
+
releaseId?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Service that manages and evaluates {@link HealthCheck}s.
|
|
34
|
+
*
|
|
35
|
+
* Checks are executed in parallel with per-check timeout.
|
|
36
|
+
* The overall status uses worst-status-wins aggregation: fail > warn > pass.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* const service = new HealthService({ version: '1.0.0' });
|
|
40
|
+
* service.addHealthCheck(new DatabaseHealthCheck());
|
|
41
|
+
* const response = await service.evaluate();
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare class HealthService {
|
|
45
|
+
readonly version: string;
|
|
46
|
+
readonly releaseId: string;
|
|
47
|
+
private readonly checks;
|
|
48
|
+
constructor(options: HealthServiceOptions);
|
|
49
|
+
/** Register a health check to be evaluated on each call to {@link evaluate}. */
|
|
50
|
+
addHealthCheck(check: HealthCheck): void;
|
|
51
|
+
/**
|
|
52
|
+
* Execute all registered checks in parallel and return an aggregated
|
|
53
|
+
* {@link HealthResponse}.
|
|
54
|
+
*/
|
|
55
|
+
evaluate(): Promise<HealthResponse>;
|
|
56
|
+
/** Run a single check with timeout and timing. */
|
|
57
|
+
private runCheck;
|
|
58
|
+
/** Execute a check with its configured timeout. */
|
|
59
|
+
private withTimeout;
|
|
60
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// core/health/health_service.ts
|
|
4
|
+
// HealthService — evaluates checks in parallel with timeout.
|
|
5
|
+
// Mirror of health_service.dart in Dart.
|
|
6
|
+
// ============================================================
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.HealthService = exports.HealthResponse = void 0;
|
|
9
|
+
const health_check_1 = require("./health_check");
|
|
10
|
+
/**
|
|
11
|
+
* Aggregated health response following the IETF Health Check Response Format.
|
|
12
|
+
*
|
|
13
|
+
* `httpStatusCode`: 200 for pass/warn, 503 for fail.
|
|
14
|
+
*/
|
|
15
|
+
class HealthResponse {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.status = options.status;
|
|
18
|
+
this.version = options.version;
|
|
19
|
+
this.releaseId = options.releaseId;
|
|
20
|
+
this.checks = options.checks;
|
|
21
|
+
}
|
|
22
|
+
/** HTTP status code: 200 for pass/warn, 503 for fail. */
|
|
23
|
+
get httpStatusCode() {
|
|
24
|
+
return this.status === 'fail' ? 503 : 200;
|
|
25
|
+
}
|
|
26
|
+
/** Serialize to the IETF-compliant JSON structure. */
|
|
27
|
+
toJson() {
|
|
28
|
+
const checksJson = {};
|
|
29
|
+
for (const [key, result] of Object.entries(this.checks)) {
|
|
30
|
+
checksJson[key] = result.toJson();
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
status: this.status,
|
|
34
|
+
version: this.version,
|
|
35
|
+
releaseId: this.releaseId,
|
|
36
|
+
checks: checksJson,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.HealthResponse = HealthResponse;
|
|
41
|
+
/**
|
|
42
|
+
* Service that manages and evaluates {@link HealthCheck}s.
|
|
43
|
+
*
|
|
44
|
+
* Checks are executed in parallel with per-check timeout.
|
|
45
|
+
* The overall status uses worst-status-wins aggregation: fail > warn > pass.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* const service = new HealthService({ version: '1.0.0' });
|
|
49
|
+
* service.addHealthCheck(new DatabaseHealthCheck());
|
|
50
|
+
* const response = await service.evaluate();
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
class HealthService {
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.checks = [];
|
|
56
|
+
this.version = options.version;
|
|
57
|
+
this.releaseId = options.releaseId ?? process.env.RELEASE_ID ?? `${options.version}-debug`;
|
|
58
|
+
}
|
|
59
|
+
/** Register a health check to be evaluated on each call to {@link evaluate}. */
|
|
60
|
+
addHealthCheck(check) {
|
|
61
|
+
this.checks.push(check);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Execute all registered checks in parallel and return an aggregated
|
|
65
|
+
* {@link HealthResponse}.
|
|
66
|
+
*/
|
|
67
|
+
async evaluate() {
|
|
68
|
+
if (this.checks.length === 0) {
|
|
69
|
+
return new HealthResponse({
|
|
70
|
+
status: 'pass',
|
|
71
|
+
version: this.version,
|
|
72
|
+
releaseId: this.releaseId,
|
|
73
|
+
checks: {},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const entries = await Promise.all(this.checks.map((c) => this.runCheck(c)));
|
|
77
|
+
const checks = {};
|
|
78
|
+
for (const [name, result] of entries) {
|
|
79
|
+
checks[name] = result;
|
|
80
|
+
}
|
|
81
|
+
// Worst-status-wins: fail > warn > pass
|
|
82
|
+
const status = Object.values(checks)
|
|
83
|
+
.map((r) => r.status)
|
|
84
|
+
.reduce(health_check_1.worstStatus);
|
|
85
|
+
return new HealthResponse({
|
|
86
|
+
status,
|
|
87
|
+
version: this.version,
|
|
88
|
+
releaseId: this.releaseId,
|
|
89
|
+
checks,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Run a single check with timeout and timing. */
|
|
93
|
+
async runCheck(check) {
|
|
94
|
+
const start = Date.now();
|
|
95
|
+
try {
|
|
96
|
+
const result = await this.withTimeout(check);
|
|
97
|
+
const elapsed = Date.now() - start;
|
|
98
|
+
return [check.name, result.withResponseTime(elapsed)];
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const elapsed = Date.now() - start;
|
|
102
|
+
return [
|
|
103
|
+
check.name,
|
|
104
|
+
new health_check_1.HealthCheckResult('fail', {
|
|
105
|
+
responseTime: elapsed,
|
|
106
|
+
output: String(err),
|
|
107
|
+
}),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Execute a check with its configured timeout. */
|
|
112
|
+
withTimeout(check) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const timer = setTimeout(() => {
|
|
115
|
+
resolve(new health_check_1.HealthCheckResult('fail', {
|
|
116
|
+
output: `Health check "${check.name}" timeout after ${check.timeout}ms`,
|
|
117
|
+
}));
|
|
118
|
+
}, check.timeout);
|
|
119
|
+
check
|
|
120
|
+
.check()
|
|
121
|
+
.then((result) => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
resolve(result);
|
|
124
|
+
})
|
|
125
|
+
.catch((err) => {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
reject(err);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.HealthService = HealthService;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 5424 log levels in descending severity order.
|
|
3
|
+
*
|
|
4
|
+
* Filtering rule: if configured `logLevel = X`, only messages with
|
|
5
|
+
* `value <= X` are emitted. Higher values produce total silence.
|
|
6
|
+
*/
|
|
7
|
+
export declare enum LogLevel {
|
|
8
|
+
emergency = 0,// system unusable
|
|
9
|
+
alert = 1,// immediate action required
|
|
10
|
+
critical = 2,// critical condition
|
|
11
|
+
error = 3,// operation error, 5xx
|
|
12
|
+
warning = 4,// abnormal condition, 4xx
|
|
13
|
+
notice = 5,// normal but significant
|
|
14
|
+
info = 6,// normal flow, 2xx/3xx
|
|
15
|
+
debug = 7
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Public logger interface exposed to UseCases.
|
|
19
|
+
*
|
|
20
|
+
* Each method corresponds to an RFC 5424 severity level.
|
|
21
|
+
* `fields` is an optional map of structured data attached to the log entry.
|
|
22
|
+
*/
|
|
23
|
+
export interface ModularLogger {
|
|
24
|
+
/** Request-scoped trace ID for correlation. */
|
|
25
|
+
readonly traceId: string;
|
|
26
|
+
emergency(msg: string, fields?: Record<string, unknown>): void;
|
|
27
|
+
alert(msg: string, fields?: Record<string, unknown>): void;
|
|
28
|
+
critical(msg: string, fields?: Record<string, unknown>): void;
|
|
29
|
+
error(msg: string, fields?: Record<string, unknown>): void;
|
|
30
|
+
warning(msg: string, fields?: Record<string, unknown>): void;
|
|
31
|
+
notice(msg: string, fields?: Record<string, unknown>): void;
|
|
32
|
+
info(msg: string, fields?: Record<string, unknown>): void;
|
|
33
|
+
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
34
|
+
}
|
|
35
|
+
/** Function signature for the output sink — defaults to `console.log`. */
|
|
36
|
+
export type WriteFn = (line: string) => void;
|
|
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
|
+
export declare class RequestScopedLogger implements ModularLogger {
|
|
47
|
+
readonly traceId: string;
|
|
48
|
+
readonly logLevel: LogLevel;
|
|
49
|
+
readonly serviceName: string;
|
|
50
|
+
private readonly writeFn;
|
|
51
|
+
constructor(traceId: string, logLevel: LogLevel, serviceName: string, writeFn?: WriteFn);
|
|
52
|
+
emergency(msg: string, fields?: Record<string, unknown>): void;
|
|
53
|
+
alert(msg: string, fields?: Record<string, unknown>): void;
|
|
54
|
+
critical(msg: string, fields?: Record<string, unknown>): void;
|
|
55
|
+
error(msg: string, fields?: Record<string, unknown>): void;
|
|
56
|
+
warning(msg: string, fields?: Record<string, unknown>): void;
|
|
57
|
+
notice(msg: string, fields?: Record<string, unknown>): void;
|
|
58
|
+
info(msg: string, fields?: Record<string, unknown>): void;
|
|
59
|
+
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
60
|
+
/** Emits a "request received" log at `info` level. */
|
|
61
|
+
logRequest(opts: {
|
|
62
|
+
method: string;
|
|
63
|
+
route: string;
|
|
64
|
+
}): void;
|
|
65
|
+
/** Emits a "request completed" log at the level determined by `statusCode`. */
|
|
66
|
+
logResponse(opts: {
|
|
67
|
+
method: string;
|
|
68
|
+
route: string;
|
|
69
|
+
statusCode: number;
|
|
70
|
+
durationMs: number;
|
|
71
|
+
}): void;
|
|
72
|
+
/** Emits an "unhandled exception" log at `error` level. No stack trace. */
|
|
73
|
+
logUnhandledException(opts: {
|
|
74
|
+
route: string;
|
|
75
|
+
}): void;
|
|
76
|
+
private log;
|
|
77
|
+
/** Maps HTTP status code → RFC 5424 log level. */
|
|
78
|
+
static levelForStatus(status: number): LogLevel;
|
|
79
|
+
}
|