@chidchanun/bcp 0.2.6 → 0.2.8

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,445 @@
1
+ # Observability Platform v2
2
+
3
+ BCP Framework `0.2.7` adds a server-only observability layer through `bcp/observability`.
4
+
5
+ The platform is dependency-free and provides:
6
+
7
+ - in-process counters, gauges and histograms,
8
+ - Prometheus text exposition,
9
+ - HTTP request count and duration middleware,
10
+ - health/readiness check registration,
11
+ - health responses with HTTP `200` / `503` semantics.
12
+
13
+ It complements the existing structured logger rather than replacing it.
14
+
15
+ ## Metrics registry
16
+
17
+ Create one application-level registry:
18
+
19
+ ```ts
20
+ import {
21
+ createMetricsRegistry,
22
+ } from "bcp/observability";
23
+
24
+ export const metrics =
25
+ createMetricsRegistry();
26
+ ```
27
+
28
+ ### Counter
29
+
30
+ ```ts
31
+ const jobs =
32
+ metrics.counter(
33
+ "app_jobs_total",
34
+ {
35
+ help:
36
+ "Jobs processed by the application.",
37
+ labelNames: [
38
+ "queue",
39
+ ],
40
+ }
41
+ );
42
+
43
+ jobs.inc(
44
+ 1,
45
+ {
46
+ queue: "email",
47
+ }
48
+ );
49
+ ```
50
+
51
+ Counters cannot be decreased.
52
+
53
+ ### Gauge
54
+
55
+ ```ts
56
+ const workers =
57
+ metrics.gauge(
58
+ "app_active_workers",
59
+ {
60
+ labelNames: [
61
+ "pool",
62
+ ],
63
+ }
64
+ );
65
+
66
+ workers.set(
67
+ 4,
68
+ {
69
+ pool: "default",
70
+ }
71
+ );
72
+ workers.inc(
73
+ 1,
74
+ {
75
+ pool: "default",
76
+ }
77
+ );
78
+ workers.dec(
79
+ 1,
80
+ {
81
+ pool: "default",
82
+ }
83
+ );
84
+ ```
85
+
86
+ ### Histogram
87
+
88
+ ```ts
89
+ const queryDuration =
90
+ metrics.histogram(
91
+ "app_query_duration_seconds",
92
+ {
93
+ labelNames: [
94
+ "operation",
95
+ ],
96
+ buckets: [
97
+ 0.01,
98
+ 0.05,
99
+ 0.1,
100
+ 0.5,
101
+ 1,
102
+ ],
103
+ }
104
+ );
105
+
106
+ queryDuration.observe(
107
+ 0.032,
108
+ {
109
+ operation: "users.list",
110
+ }
111
+ );
112
+ ```
113
+
114
+ If no buckets are supplied, BCP uses a general-purpose HTTP/runtime latency bucket set.
115
+
116
+ ## Label contract
117
+
118
+ Metric label names are declared when the metric is registered. Every update must provide exactly that set of labels.
119
+
120
+ This fails intentionally:
121
+
122
+ ```ts
123
+ const requests =
124
+ metrics.counter(
125
+ "app_requests_total",
126
+ {
127
+ labelNames: [
128
+ "method",
129
+ ],
130
+ }
131
+ );
132
+
133
+ // Missing the declared method label.
134
+ requests.inc();
135
+ ```
136
+
137
+ BCP does not automatically attach request paths, user IDs, session IDs, emails or other high-cardinality identifiers to metrics.
138
+
139
+ Avoid labels whose value set grows without a small bound.
140
+
141
+ Good examples:
142
+
143
+ ```text
144
+ method
145
+ status
146
+ queue
147
+ provider
148
+ operation
149
+ ```
150
+
151
+ Risky examples:
152
+
153
+ ```text
154
+ user_id
155
+ session_id
156
+ raw_url
157
+ email
158
+ request_id
159
+ ```
160
+
161
+ ## Prometheus exposition
162
+
163
+ Use `createMetricsResponse()` from an API route:
164
+
165
+ ```ts
166
+ import {
167
+ createMetricsResponse,
168
+ } from "bcp/observability";
169
+ import {
170
+ metrics,
171
+ } from "@/lib/observability";
172
+
173
+ export function GET() {
174
+ return createMetricsResponse(
175
+ metrics
176
+ );
177
+ }
178
+ ```
179
+
180
+ Typical output:
181
+
182
+ ```text
183
+ # HELP app_requests_total Application requests.
184
+ # TYPE app_requests_total counter
185
+ app_requests_total{method="GET"} 42
186
+ ```
187
+
188
+ The response content type is compatible with Prometheus text exposition and uses `Cache-Control: no-store`.
189
+
190
+ ### Protect the metrics endpoint
191
+
192
+ Metrics frequently expose operational information. Do not assume `/metrics` should be public.
193
+
194
+ Protect it using your deployment network, reverse proxy, authorization policy, internal service routing or another appropriate boundary.
195
+
196
+ ## HTTP request metrics middleware
197
+
198
+ Create middleware from the same application registry:
199
+
200
+ ```ts
201
+ import {
202
+ createMetricsRegistry,
203
+ createRequestMetricsMiddleware,
204
+ } from "bcp/observability";
205
+
206
+ export const metrics =
207
+ createMetricsRegistry();
208
+
209
+ export const requestMetrics =
210
+ createRequestMetricsMiddleware(
211
+ metrics
212
+ );
213
+ ```
214
+
215
+ Then include it in the application middleware pipeline:
216
+
217
+ ```ts
218
+ import {
219
+ requestMetrics,
220
+ } from "@/lib/observability";
221
+
222
+ export const middleware = [
223
+ requestMetrics,
224
+ ];
225
+ ```
226
+
227
+ Default series:
228
+
229
+ ```text
230
+ bcp_http_requests_total{method,status}
231
+ bcp_http_request_duration_seconds{method,status}
232
+ ```
233
+
234
+ The default middleware intentionally does not label requests by raw path. This avoids generating one metric series per dynamic URL or identifier.
235
+
236
+ Customize the prefix:
237
+
238
+ ```ts
239
+ createRequestMetricsMiddleware(
240
+ metrics,
241
+ {
242
+ prefix: "shop",
243
+ }
244
+ );
245
+ ```
246
+
247
+ This produces metrics beginning with `shop_`.
248
+
249
+ You can disable the method or status labels:
250
+
251
+ ```ts
252
+ createRequestMetricsMiddleware(
253
+ metrics,
254
+ {
255
+ includeMethod: false,
256
+ includeStatus: false,
257
+ }
258
+ );
259
+ ```
260
+
261
+ ## Health and readiness checks
262
+
263
+ Create an application-level health registry:
264
+
265
+ ```ts
266
+ import {
267
+ createHealthRegistry,
268
+ } from "bcp/observability";
269
+
270
+ export const health =
271
+ createHealthRegistry();
272
+ ```
273
+
274
+ Register dependencies:
275
+
276
+ ```ts
277
+ health.register(
278
+ "database",
279
+ async () => {
280
+ await db.query(
281
+ "SELECT 1"
282
+ );
283
+
284
+ return {
285
+ ok: true,
286
+ detail: "connected",
287
+ };
288
+ }
289
+ );
290
+ ```
291
+
292
+ A check may return:
293
+
294
+ ```ts
295
+ true
296
+ false
297
+ ```
298
+
299
+ or:
300
+
301
+ ```ts
302
+ {
303
+ ok: true,
304
+ detail: "connected",
305
+ }
306
+ ```
307
+
308
+ ### Health endpoint
309
+
310
+ ```ts
311
+ import {
312
+ health,
313
+ } from "@/lib/observability";
314
+
315
+ export function GET() {
316
+ return health.response();
317
+ }
318
+ ```
319
+
320
+ Healthy response:
321
+
322
+ ```text
323
+ HTTP 200
324
+ ```
325
+
326
+ If any registered check fails:
327
+
328
+ ```text
329
+ HTTP 503
330
+ ```
331
+
332
+ The JSON report contains:
333
+
334
+ ```ts
335
+ {
336
+ ok: boolean;
337
+ status: "healthy" | "unhealthy";
338
+ checkedAt: string;
339
+ checks: Array<{
340
+ name: string;
341
+ ok: boolean;
342
+ durationMs: number;
343
+ detail?: string;
344
+ }>;
345
+ }
346
+ ```
347
+
348
+ ## Health check timeout
349
+
350
+ The default timeout is 5 seconds per check.
351
+
352
+ Override it when registering a check:
353
+
354
+ ```ts
355
+ health.register(
356
+ "database",
357
+ checkDatabase,
358
+ {
359
+ timeoutMs: 1500,
360
+ }
361
+ );
362
+ ```
363
+
364
+ A timed-out or thrown check is reported as unhealthy instead of rejecting the complete health report.
365
+
366
+ Timeout does not cancel the underlying application operation. Provider-specific cancellation remains the responsibility of the check implementation.
367
+
368
+ ## Liveness vs readiness
369
+
370
+ Use lightweight liveness checks for whether the process/runtime is functioning.
371
+
372
+ Use readiness checks for dependencies required before receiving traffic, for example:
373
+
374
+ ```text
375
+ database
376
+ cache
377
+ message queue
378
+ critical upstream API
379
+ ```
380
+
381
+ A common deployment structure is:
382
+
383
+ ```text
384
+ /api/health/live
385
+ /api/health/ready
386
+ /metrics
387
+ ```
388
+
389
+ BCP provides the primitives but does not force endpoint names.
390
+
391
+ ## Process-local scope
392
+
393
+ The built-in metrics registry is process-local.
394
+
395
+ With multiple Node processes or containers:
396
+
397
+ ```text
398
+ instance A -> its own registry
399
+ instance B -> its own registry
400
+ instance C -> its own registry
401
+ ```
402
+
403
+ Prometheus-style monitoring should scrape each instance, or deployment infrastructure should aggregate metrics externally.
404
+
405
+ BCP `0.2.7` does not provide distributed metric aggregation or an OpenTelemetry exporter.
406
+
407
+ ## Relationship to logging
408
+
409
+ Structured logs remain available through `bcp/server`:
410
+
411
+ ```ts
412
+ import {
413
+ logger,
414
+ } from "bcp/server";
415
+ ```
416
+
417
+ Use logs for event detail and debugging, and metrics for aggregate system behavior.
418
+
419
+ A typical production model is:
420
+
421
+ ```text
422
+ structured logs
423
+ +
424
+ metrics
425
+ +
426
+ health/readiness
427
+ ```
428
+
429
+ ## Server-only boundary
430
+
431
+ `bcp/observability` is server-only.
432
+
433
+ Do not import it into client components or browser islands. The package browser export maps to BCP's server-only guard.
434
+
435
+ ## Release scope
436
+
437
+ `0.2.7` intentionally does not add:
438
+
439
+ - distributed tracing,
440
+ - OpenTelemetry exporters,
441
+ - hosted monitoring integrations,
442
+ - automatic raw-route labels,
443
+ - cross-process metric aggregation.
444
+
445
+ Those can be layered on later without changing the metric/health application contract introduced here.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "framework": "bcp",
4
- "version": "0.2.6",
4
+ "version": "0.2.8",
5
5
  "releaseState": "unreleased",
6
- "baseline": "authorization-security-v2",
6
+ "baseline": "background-jobs-platform",
7
7
  "runtime": {
8
8
  "node": ">=24.11.0",
9
9
  "react": "19",
@@ -19,6 +19,8 @@
19
19
  "bcp/error",
20
20
  "bcp/database",
21
21
  "bcp/auth",
22
+ "bcp/jobs",
23
+ "bcp/observability",
22
24
  "bcp/server",
23
25
  "bcp/server-only",
24
26
  "bcp/middleware"
@@ -59,6 +61,18 @@
59
61
  "permissionRouteGuards": true,
60
62
  "sameOriginProtection": true,
61
63
  "csrfProtection": true,
64
+ "observabilityPlatformV2": true,
65
+ "metricsRegistry": true,
66
+ "prometheusMetrics": true,
67
+ "requestMetricsMiddleware": true,
68
+ "healthChecks": true,
69
+ "backgroundJobsPlatform": true,
70
+ "jobQueueAdapterContract": true,
71
+ "inMemoryJobQueue": true,
72
+ "delayedJobs": true,
73
+ "jobRetries": true,
74
+ "jobWorkerConcurrency": true,
75
+ "jobCancellation": true,
62
76
  "databaseMigrations": true,
63
77
  "databaseAdapterContract": true,
64
78
  "databasePostgresql": true,
@@ -98,7 +112,7 @@
98
112
  "s3-compatible"
99
113
  ],
100
114
  "compatibility": {
101
- "previousBaseline": "0.2.5",
115
+ "previousBaseline": "0.2.7",
102
116
  "intentionalBreakingChangesFromPreviousBaseline": false,
103
117
  "migrationGuide": "migration-0.2.md"
104
118
  },
@@ -114,7 +128,9 @@
114
128
  "authentication": "authentication.md",
115
129
  "authSessionStore": "auth-session-store.md",
116
130
  "authorizationSecurity": "authorization-security.md",
131
+ "observability": "observability.md",
132
+ "backgroundJobs": "background-jobs.md",
117
133
  "migrationGuide": "migration-0.2.md",
118
- "releaseNotes": "releases/0.2.6.md"
134
+ "releaseNotes": "releases/0.2.8.md"
119
135
  }
120
136
  }
@@ -0,0 +1,205 @@
1
+ # BCP Framework 0.2.7 — Observability Platform v2
2
+
3
+ > Release state: unreleased until RC validation, tagging and npm publication complete.
4
+
5
+ BCP Framework `0.2.7` adds a dependency-free server observability platform while preserving the existing logging/runtime model.
6
+
7
+ ## Highlights
8
+
9
+ ### New `bcp/observability` entrypoint
10
+
11
+ ```ts
12
+ import {
13
+ createHealthRegistry,
14
+ createMetricsRegistry,
15
+ createMetricsResponse,
16
+ createRequestMetricsMiddleware,
17
+ } from "bcp/observability";
18
+ ```
19
+
20
+ The entrypoint is server-only and uses the same browser boundary as other server runtime APIs.
21
+
22
+ ### Metrics registry
23
+
24
+ Applications can create process-local:
25
+
26
+ - counters,
27
+ - gauges,
28
+ - histograms.
29
+
30
+ Example:
31
+
32
+ ```ts
33
+ const metrics =
34
+ createMetricsRegistry();
35
+
36
+ const requests =
37
+ metrics.counter(
38
+ "app_requests_total",
39
+ {
40
+ labelNames: [
41
+ "method",
42
+ ],
43
+ }
44
+ );
45
+
46
+ requests.inc(
47
+ 1,
48
+ {
49
+ method: "GET",
50
+ }
51
+ );
52
+ ```
53
+
54
+ Metric definitions validate names, labels and number values. Re-registering the same metric with an incompatible type, label set or histogram buckets fails explicitly.
55
+
56
+ ### Prometheus text output
57
+
58
+ ```ts
59
+ return createMetricsResponse(
60
+ metrics
61
+ );
62
+ ```
63
+
64
+ The generated response uses Prometheus-compatible text exposition metadata and `Cache-Control: no-store`.
65
+
66
+ ### HTTP request metrics middleware
67
+
68
+ ```ts
69
+ const requestMetrics =
70
+ createRequestMetricsMiddleware(
71
+ metrics
72
+ );
73
+ ```
74
+
75
+ Default metrics:
76
+
77
+ ```text
78
+ bcp_http_requests_total
79
+ bcp_http_request_duration_seconds
80
+ ```
81
+
82
+ Default labels:
83
+
84
+ ```text
85
+ method
86
+ status
87
+ ```
88
+
89
+ Raw URL paths are intentionally not included by default to avoid unbounded metric cardinality.
90
+
91
+ ### Health and readiness checks
92
+
93
+ ```ts
94
+ const health =
95
+ createHealthRegistry();
96
+
97
+ health.register(
98
+ "database",
99
+ async () => {
100
+ await db.query(
101
+ "SELECT 1"
102
+ );
103
+
104
+ return true;
105
+ }
106
+ );
107
+ ```
108
+
109
+ Health responses use:
110
+
111
+ ```text
112
+ 200 healthy
113
+ 503 unhealthy
114
+ ```
115
+
116
+ Checks run independently and include duration metadata.
117
+
118
+ ### Health timeouts
119
+
120
+ Each health check has a default timeout of 5 seconds and may configure its own timeout.
121
+
122
+ Thrown or timed-out checks become unhealthy report items rather than rejecting the whole health operation.
123
+
124
+ ## Public APIs
125
+
126
+ ### Metrics
127
+
128
+ ```text
129
+ createMetricsRegistry()
130
+ createMetricsResponse()
131
+ createRequestMetricsMiddleware()
132
+ ```
133
+
134
+ Metric interfaces:
135
+
136
+ ```text
137
+ CounterMetric
138
+ GaugeMetric
139
+ HistogramMetric
140
+ MetricsRegistry
141
+ MetricLabels
142
+ MetricDefinitionOptions
143
+ HistogramOptions
144
+ RequestMetricsOptions
145
+ ```
146
+
147
+ ### Health
148
+
149
+ ```text
150
+ createHealthRegistry()
151
+ ```
152
+
153
+ Health interfaces:
154
+
155
+ ```text
156
+ HealthRegistry
157
+ HealthCheck
158
+ HealthCheckResult
159
+ HealthCheckOptions
160
+ HealthReport
161
+ HealthCheckReportItem
162
+ ```
163
+
164
+ ## Compatibility
165
+
166
+ `0.2.7` has no intentional breaking changes from `0.2.6`.
167
+
168
+ Existing applications do not need to create a metrics or health registry. The new platform is opt-in.
169
+
170
+ Existing structured logging through `bcp/server` remains unchanged.
171
+
172
+ ## Operational notes
173
+
174
+ Metrics are process-local. Multi-process and multi-container deployments should expose/scrape metrics for each instance or aggregate through deployment infrastructure.
175
+
176
+ The built-in platform does not provide distributed aggregation or an OpenTelemetry exporter in this release.
177
+
178
+ Health check error details may contain operational information. Applications should decide whether health endpoints are publicly accessible and avoid returning secrets from custom check details.
179
+
180
+ Metrics endpoints should normally be protected by an internal network, reverse proxy or application authorization boundary when operational data is sensitive.
181
+
182
+ ## Validation coverage
183
+
184
+ `0.2.7` adds unit and prepared-package checks for:
185
+
186
+ - counter accumulation,
187
+ - gauge set/increment/decrement,
188
+ - histogram bucket/count output,
189
+ - Prometheus response metadata,
190
+ - incompatible metric registration,
191
+ - request status/method timing metrics,
192
+ - no raw path labels in default request metrics,
193
+ - healthy/unhealthy reports,
194
+ - HTTP 503 readiness behavior,
195
+ - health check timeout handling,
196
+ - `bcp/observability` npm export availability,
197
+ - documentation/platform/API manifest parity.
198
+
199
+ ## Documentation
200
+
201
+ See:
202
+
203
+ - [Observability Platform v2](../observability.md)
204
+ - [Logging](../development-logging.md)
205
+ - [API Reference](../api-reference.md)