Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions adapters/flask/app/health.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from flask import Blueprint, Response, jsonify
from flask import Blueprint, Response, current_app, jsonify

# @DB_ENGINE@

Expand All @@ -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
2 changes: 2 additions & 0 deletions adapters/laravel-api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions adapters/laravel-api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 10 additions & 1 deletion adapters/laravel-api/routes/health.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
2 changes: 2 additions & 0 deletions adapters/laravel-inertia/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions adapters/laravel-inertia/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 10 additions & 1 deletion adapters/laravel-inertia/routes/health.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
16 changes: 14 additions & 2 deletions adapters/nestjs/src/health/health.controller.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
);
}
Expand Down
19 changes: 19 additions & 0 deletions tests/service.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down