From 87227cacec4bfa9f864a64023772d99b85f1d7c0 Mon Sep 17 00:00:00 2001 From: iam-truongtrungnghia Date: Mon, 14 Sep 2026 02:21:13 +0700 Subject: [PATCH] fix(adapters): log the readiness failure instead of returning it --- adapters/flask/app/health.py | 8 ++++++-- adapters/laravel-api/.env.example | 2 ++ adapters/laravel-api/Dockerfile | 3 +++ adapters/laravel-api/routes/health.php | 11 ++++++++++- adapters/laravel-inertia/.env.example | 2 ++ adapters/laravel-inertia/Dockerfile | 3 +++ adapters/laravel-inertia/routes/health.php | 11 ++++++++++- .../nestjs/src/health/health.controller.ts | 16 ++++++++++++++-- tests/service.bats | 19 +++++++++++++++++++ 9 files changed, 69 insertions(+), 6 deletions(-) diff --git a/adapters/flask/app/health.py b/adapters/flask/app/health.py index a0eea16..ec9a52d 100644 --- a/adapters/flask/app/health.py +++ b/adapters/flask/app/health.py @@ -1,4 +1,4 @@ -from flask import Blueprint, Response, jsonify +from flask import Blueprint, Response, current_app, jsonify # @DB_ENGINE@ @@ -18,4 +18,8 @@ def ready() -> Reply: # @DB_PROBE@ raise RuntimeError("no database is configured for this project") except Exception as error: # noqa: BLE001 - return jsonify(status="unavailable", reason=str(error)), 503 + # Logged, not returned: a driver's connection error names the host, + # port, user and database, and /health/ready is unauthenticated. An + # orchestrator reads the status code and nothing else. + current_app.logger.warning("readiness probe failed: %s", error) + return jsonify(status="unavailable"), 503 diff --git a/adapters/laravel-api/.env.example b/adapters/laravel-api/.env.example index cf699ce..f521bd1 100644 --- a/adapters/laravel-api/.env.example +++ b/adapters/laravel-api/.env.example @@ -4,3 +4,5 @@ APP_KEY= APP_DEBUG=false APP_URL=http://localhost:8000 # the database variables are written by the selected service's driver +# the readiness probe logs its failure reason; storage/logs is unreachable in a container +LOG_CHANNEL=stderr diff --git a/adapters/laravel-api/Dockerfile b/adapters/laravel-api/Dockerfile index 37207a0..b7573d5 100644 --- a/adapters/laravel-api/Dockerfile +++ b/adapters/laravel-api/Dockerfile @@ -21,6 +21,9 @@ COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini # :8080 is how frankenphp listens on an unprivileged port and declines to # provision TLS, which belongs to whatever proxy this lands behind. ENV SERVER_NAME=:8080 +# laravel's default `stack` channel writes to storage/logs, which nothing +# reads in a container; the readiness probe's failure reason is logged. +ENV LOG_CHANNEL=stderr # Caddy writes here and will not start if it may not. The app tree needs the # same: COPY runs as root, the server runs as www-data, and the first request # that compiles a blade view or writes a log fails without this. diff --git a/adapters/laravel-api/routes/health.php b/adapters/laravel-api/routes/health.php index a113445..8d76da3 100644 --- a/adapters/laravel-api/routes/health.php +++ b/adapters/laravel-api/routes/health.php @@ -16,6 +16,15 @@ // @DB_PROBE@ throw new RuntimeException('no database is configured for this project'); } catch (Throwable $e) { - return response()->json(['status' => 'unavailable', 'reason' => $e->getMessage()], 503); + // Logged, not returned: a driver's connection error names the host, + // port, user and database, and /health/ready is unauthenticated. An + // orchestrator reads the status code and nothing else. logger(), not + // the Log facade: an import here would sit between the two `use` + // lines services/mongodb/drivers/laravel.sh splices, and pint's + // ordered_imports would then fail the generated project's own format + // task. + logger()->warning('readiness probe failed: '.$e->getMessage()); + + return response()->json(['status' => 'unavailable'], 503); } }); diff --git a/adapters/laravel-inertia/.env.example b/adapters/laravel-inertia/.env.example index 5f403cd..ea0ecbd 100644 --- a/adapters/laravel-inertia/.env.example +++ b/adapters/laravel-inertia/.env.example @@ -5,3 +5,5 @@ APP_DEBUG=false APP_URL=http://localhost:8000 # the database variables are written by the selected service's driver VITE_APP_NAME="${APP_NAME}" +# the readiness probe logs its failure reason; storage/logs is unreachable in a container +LOG_CHANNEL=stderr diff --git a/adapters/laravel-inertia/Dockerfile b/adapters/laravel-inertia/Dockerfile index a1d4bd3..fedd27e 100644 --- a/adapters/laravel-inertia/Dockerfile +++ b/adapters/laravel-inertia/Dockerfile @@ -37,6 +37,9 @@ COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini # :8080 is how frankenphp listens on an unprivileged port and declines to # provision TLS, which belongs to whatever proxy this lands behind. ENV SERVER_NAME=:8080 +# laravel's default `stack` channel writes to storage/logs, which nothing +# reads in a container; the readiness probe's failure reason is logged. +ENV LOG_CHANNEL=stderr # Caddy writes here and will not start if it may not. The app tree needs the # same: COPY runs as root, the server runs as www-data, and the first request # that compiles a blade view or writes a log fails without this. diff --git a/adapters/laravel-inertia/routes/health.php b/adapters/laravel-inertia/routes/health.php index a113445..8d76da3 100644 --- a/adapters/laravel-inertia/routes/health.php +++ b/adapters/laravel-inertia/routes/health.php @@ -16,6 +16,15 @@ // @DB_PROBE@ throw new RuntimeException('no database is configured for this project'); } catch (Throwable $e) { - return response()->json(['status' => 'unavailable', 'reason' => $e->getMessage()], 503); + // Logged, not returned: a driver's connection error names the host, + // port, user and database, and /health/ready is unauthenticated. An + // orchestrator reads the status code and nothing else. logger(), not + // the Log facade: an import here would sit between the two `use` + // lines services/mongodb/drivers/laravel.sh splices, and pint's + // ordered_imports would then fail the generated project's own format + // task. + logger()->warning('readiness probe failed: '.$e->getMessage()); + + return response()->json(['status' => 'unavailable'], 503); } }); diff --git a/adapters/nestjs/src/health/health.controller.ts b/adapters/nestjs/src/health/health.controller.ts index 1992830..1512aac 100644 --- a/adapters/nestjs/src/health/health.controller.ts +++ b/adapters/nestjs/src/health/health.controller.ts @@ -1,7 +1,15 @@ -import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common'; +import { + Controller, + Get, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; @Controller('health') export class HealthController { + private readonly logger = new Logger(HealthController.name); + // The client field and the probe below are both written by the selected // service's driver: prisma has no provider-agnostic read, so a SQL // provider gets $queryRawUnsafe and mongodb gets $runCommandRaw. A project @@ -33,8 +41,12 @@ export class HealthController { // @DB_PROBE@ throw new Error('no database is configured for this project'); } catch (error) { + // Logged, not returned: a driver's connection error names the host, + // port, user and database, and /health/ready is unauthenticated. An + // orchestrator reads the status code and nothing else. + this.logger.warn(`readiness probe failed: ${(error as Error).message}`); throw new HttpException( - { status: 'unavailable', reason: (error as Error).message }, + { status: 'unavailable' }, HttpStatus.SERVICE_UNAVAILABLE, ); } diff --git a/tests/service.bats b/tests/service.bats index d0fa0cf..a37eda3 100644 --- a/tests/service.bats +++ b/tests/service.bats @@ -151,6 +151,25 @@ setup() { done } +@test "no readiness route hands its exception message to the caller" { + # A driver's connection error names the host, its address, the port, the user + # and the database, and /health/ready is unauthenticated. Every consumer of a + # readiness endpoint reads the status code and discards the body. + local file found=0 + for file in "${SCAFFOLD_ROOT}"/adapters/*/routes/health.php \ + "${SCAFFOLD_ROOT}"/adapters/*/src/health/health.controller.ts \ + "${SCAFFOLD_ROOT}"/adapters/*/app/health.py; do + [ -f "$file" ] || continue + found=$((found + 1)) + if grep -Eq "'reason' =>|reason:|reason=" "$file"; then + echo "${file} returns a reason field to the caller" + return 1 + fi + done + # Without this the globs matching nothing would read as every route passing. + [ "$found" -ge 4 ] +} + @test "the nest probe guard rejects a controller whose fallback throw survived" { # The guard used to check for `return { status: 'ok' };`, which live() already # returns — so a controller with three splices applied and the fourth missed