@zaaxch/tailframe 4.0.3 → 4.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaaxch/tailframe",
3
- "version": "4.0.3",
3
+ "version": "4.0.5",
4
4
  "description": "Tailframe architecture toolkit: validates the Tailframe structure, import-boundary, and file-convention contracts. The package version is the contract version.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/new.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  healthRoutesSource,
19
19
  healthSchemasSource,
20
20
  httpErrorsSource,
21
+ observabilitySource,
21
22
  mongoReadinessProbeSource,
22
23
  postgresReadinessProbeSource,
23
24
  readinessProbeSource,
@@ -221,6 +222,7 @@ const svcDeps = {
221
222
  joi: "^17.13.3",
222
223
  "reflect-metadata": "^0.2.2",
223
224
  tsyringe: "^4.9.1",
225
+ winston: "^3.19.0",
224
226
  ...(postgres ? { pg: "^8.16.3" } : { mongodb: "^6.17.0" }),
225
227
  ...(options.auth === "firebase" ? { "firebase-admin": "^13.0.0" } : {}),
226
228
  ...(options.redis ? { "rate-limiter-flexible": "^11.2.0", redis: "^5.10.0" } : {})
@@ -331,6 +333,7 @@ if (options.redis) add(`${svc}/src/platform/redis/index.ts`, redisSource);
331
333
  add(`${svc}/src/core/RequestContext.ts`, requestContextSource);
332
334
  add(`${svc}/src/core/UseCase.ts`, useCaseSource);
333
335
  add(`${svc}/src/core/errors.ts`, appErrorSource);
336
+ add(`${svc}/src/app/observability.ts`, observabilitySource);
334
337
  add(`${svc}/src/platform/http/createRequestContext.ts`, createRequestContextSource(options.auth));
335
338
  if (options.auth === "firebase") add(`${svc}/src/platform/auth/firebase.ts`, firebaseSource);
336
339
  add(`${svc}/src/platform/http/rpc.ts`, rpcSource);
@@ -480,7 +483,7 @@ These conventions apply to \`${svc}\`. This service is a modular monolith organi
480
483
  ## Architecture vocabulary
481
484
  - Use \`src/app\` for process assembly, server creation, route mounting, and dependency registration.
482
485
  - Use \`src/core\` only for small technology-neutral contracts such as \`RequestContext\` and \`UseCase\`.
483
- - Use \`src/platform\` for technical mechanisms such as Express, authentication, configuration, databases, Redis, and shared HTTP behavior.
486
+ - Use \`src/platform\` for technical mechanisms such as Express, authentication, configuration, databases, Redis, structured logging, and shared HTTP behavior.
484
487
  - Use \`src/modules/<module>\` for product capabilities.
485
488
  - These are the only service architecture roots. Keep \`src/core\` flat. Put external-system adapters under \`src/platform/integrations/<provider>\`; directories immediately under \`src/app\` are limited to \`cli\`, \`jobs\`, \`mcp\`, \`scheduled-tasks\`, \`workers\`, and \`__tests__\`.
486
489
  - Inside a module, use \`use-cases/\`, \`http/\`, and tests first. Add \`domain/\` or \`persistence/\` only when behavior requires them.
@@ -496,6 +499,7 @@ These conventions apply to \`${svc}\`. This service is a modular monolith organi
496
499
  | Express route or request schema | Owning module's \`http\` |
497
500
  | Vendor or external-system adapter | \`src/platform/integrations/<provider>\` |
498
501
  | Database, Redis, authentication, or HTTP mechanism | \`src/platform\` |
502
+ | Product-specific Winston transport | \`src/app/observability.ts\` |
499
503
  | Process lifecycle, registration, schedule, or consumer | \`src/app/<entry-point-kind>\` |
500
504
  | Cross-entry-point technology-neutral contract | Flat \`src/core\` |
501
505
  | Root executable | Bootstrap of matching \`src/app\` assembly only |
@@ -544,6 +548,7 @@ ${postgres
544
548
  - Keep optional entry-point assembly under \`src/app/<entry-point-kind>\`; root files such as \`src/server.ts\` and \`src/worker.ts\` may only bootstrap that assembly.
545
549
  - Initialize infrastructure first, then call \`registerDependencies(...)\` from every process entry point. The composition root resets the container, explicitly constructs dependencies, and registers class-token instances. Keep tsyringe, injection tokens, decorators, and container resolution out of modules and platform adapters.
546
550
  - Register routes, workers, and jobs explicitly. Shutdown paths are awaitable and close resources without forcing \`process.exit\`.
551
+ - The canonical Winston JSON console transport logs every HTTP failure with safe request metadata. Add product-specific transports only in \`src/app/observability.ts\`; do not remove, silence, or reconfigure the canonical transport. Preserve diagnostic causes on translated errors without exposing them in client envelopes.
547
552
  - Scheduled work prevents unintended overlap, stops intake before shutdown, awaits the active run, destroys its scheduler or consumer, and only then closes Redis and database clients.
548
553
  ${options.ui ? "- In production, serve the compiled UI from `dist/public` with a non-API SPA fallback. Never return the SPA for `/api` requests." : ""}
549
554
 
@@ -551,7 +556,7 @@ ${options.ui ? "- In production, serve the compiled UI from `dist/public` with a
551
556
  The ECR build targets \`linux/amd64\`. ${options.ui ? "Its default immutable tag is `<product-sha>` and production Compose requires that exact tag." : "Its default immutable tag is `<product-sha>` and production Compose requires that exact tag."} The runtime image contains production dependencies only and runs as the Node user.${options.ui && options.auth === "firebase" ? ` The build requires \`${ui}/.env.production\` and mounts it as a BuildKit secret only while Vite compiles the browser bundle; it is not copied into the final image.` : ""}
552
557
 
553
558
  ## Production deployment
554
- Pull requests into product \`main\` validate release candidates. The resulting \`main\` push validates again before GitHub Actions may publish an immutable image tagged with the full product commit SHA. Image publication and production deployment are separate workflows with distinct AWS roles: the release role publishes, while the pull-only deployment role manually deploys from \`main\` using an already-published full SHA from \`main\` history and that commit's deployment files. Product data migrations must authenticate to the registry, pull and digest-verify that exact release image, and explicitly select the production datastore; never inherit rehearsal image or target defaults. GitHub Actions may replace files under \`/opt/${options.name}/current\` and MUST NOT replace persistent configuration, credentials, keyfiles, or data under \`/opt/${options.name}/shared\`. Keep Node runtime behavior in the shared service \`.env.production\`, Compose interpolation and infrastructure credentials in \`.env.infrastructure\`, and fixed topology wiring in Compose \`environment:\`. The optional UI build-time \`.env.production\` is a separate ephemeral file. Do not run deployment until the host and GitHub environments have been bootstrapped according to \`docs/production-deployment.md\`.
559
+ Pull requests into product \`main\` validate release candidates. The resulting \`main\` push validates again before GitHub Actions may publish an immutable image tagged with the full product commit SHA. Image publication and production deployment are separate workflows with distinct AWS roles: the release role publishes, while the pull-only deployment role manually deploys from \`main\` using an already-published full SHA from \`main\` history and that commit's deployment files. Build both role-trust subjects from GitHub's API-reported \`sub_claim_prefix\`; never infer the effective prefix from \`use_immutable_subject\`. Product data migrations must authenticate to the registry, pull and digest-verify that exact release image, and explicitly select the production datastore; never inherit rehearsal image or target defaults. GitHub Actions may replace files under \`/opt/${options.name}/current\` and MUST NOT replace persistent configuration, credentials, keyfiles, or data under \`/opt/${options.name}/shared\`. Keep Node runtime behavior in the shared service \`.env.production\`, Compose interpolation and infrastructure credentials in \`.env.infrastructure\`, and fixed topology wiring in Compose \`environment:\`. The optional UI build-time \`.env.production\` is a separate ephemeral file. Do not run deployment until the host and GitHub environments have been bootstrapped according to \`docs/production-deployment.md\`.
555
560
 
556
561
  ## Tests and validation
557
562
  Run \`pnpm validate:architecture\` and \`pnpm format:check\` after changing files or imports. The generated service also includes unit tests and ${postgres ? "PostgreSQL-backed" : "Mongo-backed"} integration tests. Run focused service database tests through pnpm for the full service check. Test use cases, policies, ${postgres ? "PostgreSQL" : "Mongo"} repository adapters, and HTTP boundaries where behavior lives. Every public operation needs success and error-envelope coverage; authenticated operations need trusted-identity coverage; persisted capabilities need ${postgres ? "PostgreSQL" : "Mongo"} ownership/query coverage; UI-enabled services must verify that \`/api\` paths never fall through to the SPA. All AWS/ECR and Docker commands, including the release image script, must run outside the sandbox from the first attempt.
@@ -1209,10 +1214,18 @@ Configure \`DEPLOY_HOST\` and \`DEPLOY_USER\` as production-environment variable
1209
1214
  - \`DEPLOY_SSH_PRIVATE_KEY\`: dedicated host deployment key.
1210
1215
  - \`DEPLOY_SSH_KNOWN_HOSTS\`: pinned host key.
1211
1216
 
1212
- Restrict both environments' deployment branches to \`main\`. The release role trust accepts only the repository's
1213
- \`release\` environment OIDC subject; the pull-only deployment role accepts only its \`production\` subject. Use the
1214
- repository's active GitHub subject format: legacy subjects name the owner/repository, while immutable subjects use
1215
- their IDs. Do not use a repository-wide wildcard. Required reviewer approval MAY add a second manual gate.
1217
+ Restrict both environments' deployment branches to \`main\`. Before creating or updating either AWS role trust, query
1218
+ GitHub's repository OIDC endpoint and use its effective prefix:
1219
+
1220
+ \`\`\`bash
1221
+ gh api /repos/OWNER/REPOSITORY/actions/oidc/customization/sub --jq .sub_claim_prefix
1222
+ \`\`\`
1223
+
1224
+ Treat the returned \`sub_claim_prefix\` as authoritative; do not infer it from \`use_immutable_subject\`, repository
1225
+ age, names, or separately queried IDs. The release role trust uses exactly
1226
+ \`<sub_claim_prefix>:environment:release\`; the pull-only deployment role uses exactly
1227
+ \`<sub_claim_prefix>:environment:production\`. Keep the audience equal to \`sts.amazonaws.com\` and do not use a
1228
+ repository-wide wildcard subject. Required reviewer approval MAY add a second manual gate.
1216
1229
 
1217
1230
  ## Deploy and rollback
1218
1231
 
@@ -5,6 +5,7 @@ import {
5
5
  csrfSource,
6
6
  firebaseSource,
7
7
  httpErrorsSource,
8
+ loggerSource,
8
9
  mongoUsersSource,
9
10
  rateLimitSource,
10
11
  readEnvSource,
@@ -77,6 +78,11 @@ it("maps verified and unverified Firebase email claims and verifies only once",
77
78
 
78
79
  const errorTranslationTestSource = `import { AppError, type FailureKind } from "@/core/errors";
79
80
  import { errorHandler } from "@/platform/http/errors";
81
+ import { logger } from "@/platform/logging/logger";
82
+
83
+ jest.mock("@/platform/logging/logger", () => ({ logger: { log: jest.fn() }, serializeError: (error: unknown) => error }));
84
+
85
+ const log = logger.log as jest.MockedFunction<typeof logger.log>;
80
86
 
81
87
  const statuses: Array<[FailureKind, number]> = [
82
88
  ["invalid", 400],
@@ -91,11 +97,50 @@ const statuses: Array<[FailureKind, number]> = [
91
97
  ];
92
98
 
93
99
  it.each(statuses)("translates %s to HTTP %i", (kind, status) => {
100
+ log.mockClear();
94
101
  const json = jest.fn();
95
102
  const response = { status: jest.fn(() => ({ json })) } as never;
96
- errorHandler(new AppError("CODE", "message", kind), {} as never, response, jest.fn());
103
+ const request = { method: "POST", path: "/health.get", requestContext: { requestId: "request-1" } } as never;
104
+ errorHandler(new AppError({ code: "CODE", message: "message", kind }), request, response, jest.fn());
97
105
  expect((response as { status: jest.Mock }).status).toHaveBeenCalledWith(status);
98
106
  expect(json).toHaveBeenCalledWith({ error: { code: "CODE", message: "message" } });
107
+ expect(log).toHaveBeenCalledWith(expect.objectContaining({
108
+ level: status >= 500 ? "error" : "warn",
109
+ event: "http.request.failed",
110
+ requestId: "request-1",
111
+ code: "CODE",
112
+ kind
113
+ }));
114
+ });
115
+
116
+ it("logs an unknown failure without exposing it to the client", () => {
117
+ log.mockClear();
118
+ const json = jest.fn();
119
+ const response = { status: jest.fn(() => ({ json })) } as never;
120
+ const error = new Error("database disconnected");
121
+ errorHandler(error, { method: "POST", path: "/health.get" } as never, response, jest.fn());
122
+ expect(json).toHaveBeenCalledWith({ error: { code: "INTERNAL", message: "Internal server error" } });
123
+ expect(log).toHaveBeenCalledWith(expect.objectContaining({
124
+ level: "error",
125
+ code: "INTERNAL",
126
+ error: expect.objectContaining({ message: "database disconnected" })
127
+ }));
128
+ });
129
+
130
+ it("logs a preserved application-error cause", () => {
131
+ log.mockClear();
132
+ const json = jest.fn();
133
+ const response = { status: jest.fn(() => ({ json })) } as never;
134
+ const cause = new Error("Firebase token verification failed");
135
+ errorHandler(
136
+ AppError.unauthenticated(cause),
137
+ { method: "POST", path: "/health.get" } as never,
138
+ response,
139
+ jest.fn()
140
+ );
141
+ expect(log).toHaveBeenCalledWith(expect.objectContaining({
142
+ error: expect.objectContaining({ cause: expect.objectContaining({ message: "Firebase token verification failed" }) })
143
+ }));
99
144
  });
100
145
  `;
101
146
 
@@ -188,6 +233,7 @@ export function ownedSources(config) {
188
233
  "src/core/errors.ts": appErrorSource,
189
234
  "src/core/SchemaLifecycle.ts": schemaLifecycleSource,
190
235
  "src/platform/config/readEnv.ts": readEnvSource,
236
+ "src/platform/logging/logger.ts": loggerSource,
191
237
  "src/platform/http/createRequestContext.ts": createRequestContextSource(profiles.has("firebase") ? "firebase" : "none"),
192
238
  "src/platform/http/csrf.ts": csrfSource,
193
239
  "src/platform/http/errors.ts": httpErrorsSource,
@@ -43,20 +43,61 @@ export const appErrorSource = `export type FailureKind =
43
43
  | "unavailable"
44
44
  | "internal";
45
45
 
46
+ export interface AppErrorOptions {
47
+ code: string;
48
+ message: string;
49
+ kind: FailureKind;
50
+ details?: unknown;
51
+ cause?: unknown;
52
+ }
53
+
46
54
  export class AppError extends Error {
47
55
  readonly name = "AppError";
56
+ readonly code: string;
57
+ readonly kind: FailureKind;
58
+ readonly details: unknown;
59
+
60
+ constructor({ code, message, kind, details, cause }: AppErrorOptions) {
61
+ super(message, cause === undefined ? undefined : { cause });
62
+ this.code = code;
63
+ this.kind = kind;
64
+ this.details = details;
65
+ }
48
66
 
49
- constructor(
50
- readonly code: string,
51
- message: string,
52
- readonly kind: FailureKind,
53
- readonly details?: unknown
54
- ) {
55
- super(message);
67
+ static unauthenticated(cause?: unknown): AppError {
68
+ return new AppError({ code: "UNAUTHENTICATED", message: "Unauthorized", kind: "unauthenticated", cause });
56
69
  }
57
70
  }
58
71
  `;
59
72
 
73
+ export const loggerSource = `import { createLogger, format, transports } from "winston";
74
+
75
+ export function serializeError(error: unknown, depth = 0): unknown {
76
+ if (!(error instanceof Error)) return error;
77
+ return {
78
+ name: error.name,
79
+ message: error.message,
80
+ stack: error.stack,
81
+ ...(error.cause === undefined || depth >= 3 ? {} : { cause: serializeError(error.cause, depth + 1) })
82
+ };
83
+ }
84
+
85
+ const serializeErrorMetadata = format((info) => {
86
+ if (info.error !== undefined) info.error = serializeError(info.error);
87
+ return info;
88
+ });
89
+
90
+ /**
91
+ * The service-wide logger. Tailframe always retains the JSON console transport so
92
+ * canonical failure logging cannot be disabled by application assembly.
93
+ */
94
+ export const logger = createLogger({
95
+ level: "info",
96
+ format: format.combine(format.timestamp(), format.errors({ stack: true }), serializeErrorMetadata(), format.json()),
97
+ transports: [new transports.Console()]
98
+ });
99
+ `;
100
+
60
101
  export function createRequestContextSource(auth) {
61
102
  if (auth === "firebase") {
62
103
  return `import { randomUUID } from "node:crypto";
@@ -88,7 +129,7 @@ export async function createRequestContext(req: Request): Promise<RequestContext
88
129
  return context;
89
130
  }
90
131
  if (!/^Bearer [^\\s]+$/.test(header)) {
91
- throw new AppError("UNAUTHENTICATED", "Unauthorized", "unauthenticated");
132
+ throw AppError.unauthenticated();
92
133
  }
93
134
 
94
135
  try {
@@ -99,8 +140,8 @@ export async function createRequestContext(req: Request): Promise<RequestContext
99
140
  };
100
141
  req.requestContext = context;
101
142
  return context;
102
- } catch {
103
- throw new AppError("UNAUTHENTICATED", "Unauthorized", "unauthenticated");
143
+ } catch (error) {
144
+ throw AppError.unauthenticated(error);
104
145
  }
105
146
  }
106
147
 
@@ -200,9 +241,10 @@ export function rpcHandler<Input, Output, WireInput = Input>(
200
241
  }
201
242
  `;
202
243
 
203
- export const httpErrorsSource = `import type { ErrorRequestHandler } from "express";
244
+ export const httpErrorsSource = `import type { ErrorRequestHandler, Request } from "express";
204
245
  import Joi from "joi";
205
246
  import { AppError } from "@/core/errors";
247
+ import { logger, serializeError } from "@/platform/logging/logger";
206
248
 
207
249
  const statusByKind: Readonly<Record<AppError["kind"], number>> = {
208
250
  invalid: 400,
@@ -216,9 +258,26 @@ const statusByKind: Readonly<Record<AppError["kind"], number>> = {
216
258
  internal: 500
217
259
  };
218
260
 
219
- export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
261
+ function logFailure(req: Request, error: unknown, status: number, code: string, kind?: AppError["kind"]) {
262
+ logger.log({
263
+ level: status >= 500 ? "error" : "warn",
264
+ message: "HTTP request failed",
265
+ event: "http.request.failed",
266
+ requestId: req.requestContext?.requestId,
267
+ method: req.method,
268
+ path: req.path,
269
+ status,
270
+ code,
271
+ ...(kind === undefined ? {} : { kind }),
272
+ error: serializeError(error)
273
+ });
274
+ }
275
+
276
+ export const errorHandler: ErrorRequestHandler = (error, req, res, _next) => {
220
277
  if (error instanceof AppError) {
221
- res.status(statusByKind[error.kind]).json({
278
+ const status = statusByKind[error.kind];
279
+ logFailure(req, error, status, error.code, error.kind);
280
+ res.status(status).json({
222
281
  error: {
223
282
  code: error.code,
224
283
  message: error.message,
@@ -228,6 +287,7 @@ export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
228
287
  return;
229
288
  }
230
289
  if (Joi.isError(error)) {
290
+ logFailure(req, error, 400, "VALIDATION_FAILED", "invalid");
231
291
  res.status(400).json({
232
292
  error: {
233
293
  code: "VALIDATION_FAILED",
@@ -237,11 +297,22 @@ export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
237
297
  });
238
298
  return;
239
299
  }
240
- console.error(error);
300
+ logFailure(req, error, 500, "INTERNAL", "internal");
241
301
  res.status(500).json({ error: { code: "INTERNAL", message: "Internal server error" } });
242
302
  };
243
303
  `;
244
304
 
305
+ export const observabilitySource = `import type { Logger } from "winston";
306
+
307
+ /**
308
+ * Add product-specific Winston transports here. Do not remove or reconfigure the
309
+ * Tailframe console transport: it is the required baseline for canonical errors.
310
+ */
311
+ export function configureObservability(_logger: Pick<Logger, "add">): void {
312
+ // Add product-specific transports and leave the canonical console transport intact.
313
+ }
314
+ `;
315
+
245
316
  export const csrfSource = `import type { RequestHandler } from "express";
246
317
  import { AppError } from "@/core/errors";
247
318
 
@@ -255,7 +326,7 @@ export function csrfHeaderGuard(exemptPaths: readonly string[] = []): RequestHan
255
326
  !exempt.has(req.path) &&
256
327
  req.headers["x-requested-with"] !== "XMLHttpRequest"
257
328
  ) {
258
- next(new AppError("CSRF_HEADER_MISSING", "CSRF header missing", "forbidden"));
329
+ next(new AppError({ code: "CSRF_HEADER_MISSING", message: "CSRF header missing", kind: "forbidden" }));
259
330
  return;
260
331
  }
261
332
  next();
@@ -271,6 +342,7 @@ export const schemaLifecycleSource = `export interface SchemaLifecycle {
271
342
 
272
343
  export const applySchemaCliSource = `import "reflect-metadata";
273
344
  import { schemaLifecycle } from "@/app/schema";
345
+ import { logger } from "@/platform/logging/logger";
274
346
 
275
347
  async function main() {
276
348
  try {
@@ -281,7 +353,7 @@ async function main() {
281
353
  }
282
354
 
283
355
  void main().catch((error) => {
284
- console.error(error instanceof Error ? error.message : error);
356
+ logger.error("Schema application failed", { event: "schema.apply.failed", error });
285
357
  process.exitCode = 1;
286
358
  });
287
359
  `;
@@ -343,9 +415,9 @@ export function createRateLimiter(client: RedisClientType, policy: RateLimitPoli
343
415
  next();
344
416
  } catch (error) {
345
417
  if (error instanceof RateLimiterRes)
346
- next(new AppError("RATE_LIMITED", "Too many requests", "rate_limited"));
418
+ next(new AppError({ code: "RATE_LIMITED", message: "Too many requests", kind: "rate_limited" }));
347
419
  else if (error instanceof AppError) next(error);
348
- else next(new AppError("RATE_LIMIT_UNAVAILABLE", "Request protection unavailable", "unavailable"));
420
+ else next(new AppError({ code: "RATE_LIMIT_UNAVAILABLE", message: "Request protection unavailable", kind: "unavailable", cause: error }));
349
421
  }
350
422
  };
351
423
  }
@@ -426,8 +498,8 @@ export class GetReadiness implements UseCase<Record<string, never>, ReadinessRes
426
498
  try {
427
499
  await Promise.all(this.probes.map((probe) => probe.check()));
428
500
  return { status: "ready" };
429
- } catch {
430
- throw new AppError("NOT_READY", "Service is not ready", "unavailable");
501
+ } catch (error) {
502
+ throw new AppError({ code: "NOT_READY", message: "Service is not ready", kind: "unavailable", cause: error });
431
503
  }
432
504
  }
433
505
  }
@@ -496,9 +568,10 @@ export class RedisReadinessProbe implements ReadinessProbe {
496
568
 
497
569
  export const redisSource = `import { createClient, type RedisClientType } from "redis";
498
570
  import { env } from "@/platform/config/env";
571
+ import { logger } from "@/platform/logging/logger";
499
572
 
500
573
  export const redis: RedisClientType = createClient({ url: env.redisUrl });
501
- redis.on("error", (error) => console.error("Redis client error", error));
574
+ redis.on("error", (error) => logger.error("Redis client error", { event: "redis.client.error", error }));
502
575
 
503
576
  export async function connectRedis() {
504
577
  if (!redis.isOpen) await redis.connect();
@@ -563,10 +636,12 @@ export function serverSource({ ui, redis, csrf }) {
563
636
  return `${ui ? 'import path from "node:path";\n' : ""}import express from "express";
564
637
  import cors from "cors";
565
638
  import { registerDependencies } from "@/app/container";
639
+ import { configureObservability } from "@/app/observability";
566
640
  import { applicationRoutes } from "@/app/routes";
567
641
  import { env } from "@/platform/config/env";
568
642
  import { closeDatabase, connectDatabase } from "@/platform/database";
569
643
  ${redis ? 'import { closeRedis, connectRedis } from "@/platform/redis";\n' : ""}${csrf ? 'import { csrfHeaderGuard } from "@/platform/http/csrf";\n' : ""}import { errorHandler } from "@/platform/http/errors";
644
+ import { logger } from "@/platform/logging/logger";
570
645
 
571
646
  export interface ApplicationOptions {
572
647
  ${ui ? "\tpublicPath?: string;\n\tproduction?: boolean;\n" : ""}}
@@ -582,43 +657,57 @@ ${ui ? '\tif (production) {\n\t\tapp.get(/^(?!\\/api(?:\\/|$)).*/, (_req, res) =
582
657
  }
583
658
 
584
659
  export async function startServer() {
585
- const db = await connectDatabase();
586
- ${redis ? "\tconst redis = await connectRedis();\n" : ""} registerDependencies({ db${redis ? ", redis" : ""} });
587
- const app = createApplication();
588
- const server = app.listen(env.port);
589
- const shutdown = async () => {
590
- await new Promise<void>((resolve, reject) => {
591
- server.close((error) => (error ? reject(error) : resolve()));
592
- });
593
- ${redis ? "\t\tawait closeRedis();\n" : ""} await closeDatabase();
594
- };
595
- process.once("SIGINT", () => void shutdown());
596
- process.once("SIGTERM", () => void shutdown());
597
- return { app, server, shutdown };
660
+ try {
661
+ configureObservability(logger);
662
+ const db = await connectDatabase();
663
+ ${redis ? "\t\tconst redis = await connectRedis();\n" : ""} registerDependencies({ db${redis ? ", redis" : ""} });
664
+ const app = createApplication();
665
+ const server = app.listen(env.port);
666
+ const shutdown = async () => {
667
+ await new Promise<void>((resolve, reject) => {
668
+ server.close((error) => (error ? reject(error) : resolve()));
669
+ });
670
+ ${redis ? "\t\t\tawait closeRedis();\n" : ""} await closeDatabase();
671
+ };
672
+ process.once("SIGINT", () => void shutdown());
673
+ process.once("SIGTERM", () => void shutdown());
674
+ return { app, server, shutdown };
675
+ } catch (error) {
676
+ logger.error("Server startup failed", { event: "server.start.failed", error });
677
+ throw error;
678
+ }
598
679
  }
599
680
  `;
600
681
  }
601
682
 
602
683
  export function workerSource() {
603
684
  return `import { container, registerDependencies } from "@/app/container";
685
+ import { configureObservability } from "@/app/observability";
604
686
  import { GetReadiness } from "@/modules/health/use-cases/GetReadiness";
605
687
  import { closeDatabase, connectDatabase } from "@/platform/database";
606
688
  import { systemRequestContext } from "@/platform/http/createRequestContext";
689
+ import { logger } from "@/platform/logging/logger";
607
690
  import { closeRedis, connectRedis } from "@/platform/redis";
608
691
 
609
692
  export async function startWorker() {
610
- const db = await connectDatabase();
611
- const redis = await connectRedis();
612
- registerDependencies({ db, redis });
613
- await container.resolve(GetReadiness).execute(systemRequestContext(), {});
614
-
615
- const shutdown = async () => {
616
- await closeRedis();
617
- await closeDatabase();
618
- };
619
- process.once("SIGINT", () => void shutdown());
620
- process.once("SIGTERM", () => void shutdown());
621
- return { shutdown };
693
+ try {
694
+ configureObservability(logger);
695
+ const db = await connectDatabase();
696
+ const redis = await connectRedis();
697
+ registerDependencies({ db, redis });
698
+ await container.resolve(GetReadiness).execute(systemRequestContext(), {});
699
+
700
+ const shutdown = async () => {
701
+ await closeRedis();
702
+ await closeDatabase();
703
+ };
704
+ process.once("SIGINT", () => void shutdown());
705
+ process.once("SIGTERM", () => void shutdown());
706
+ return { shutdown };
707
+ } catch (error) {
708
+ logger.error("Worker startup failed", { event: "worker.start.failed", error });
709
+ throw error;
710
+ }
622
711
  }
623
712
  `;
624
713
  }