diff --git a/app/exceptions/base.py b/app/exceptions/base.py
index 5b583bf..e67f74b 100644
--- a/app/exceptions/base.py
+++ b/app/exceptions/base.py
@@ -1,12 +1,11 @@
from http import HTTPStatus
+import logfire
from fastapi import Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, Response
from starlette.exceptions import HTTPException
-from app.logger import logger
-
_PROBLEM_JSON_MEDIA_TYPE = "application/problem+json"
@@ -69,10 +68,10 @@ def base_exception_handler(request: Request, exc: Exception) -> Response:
media_type=_PROBLEM_JSON_MEDIA_TYPE,
)
case _:
- logger.exception(
- "Unhandled exception while processing %s",
- request.url,
- exc=exc,
+ logfire.exception(
+ "Unhandled exception while processing {url}",
+ url=str(request.url),
+ _exc_info=exc,
)
return JSONResponse(
status_code=500,
diff --git a/app/logger/__init__.py b/app/logger/__init__.py
index 5bde31a..b806d2e 100644
--- a/app/logger/__init__.py
+++ b/app/logger/__init__.py
@@ -1,3 +1,3 @@
-from app.logger.logger import debug, error, exception, info, setup_logging, warning
+from app.logger.configuration import setup_logging
-__all__ = ["debug", "error", "exception", "info", "setup_logging", "warning"]
+__all__ = ["setup_logging"]
diff --git a/app/logger/configuration.py b/app/logger/configuration.py
index f1a6d06..7de92cb 100644
--- a/app/logger/configuration.py
+++ b/app/logger/configuration.py
@@ -3,7 +3,6 @@
from logging import Handler, Logger
from app.logger.handlers import LOG_LEVEL
-from app.settings import app_settings
from app.telemetry import get_otel_log_handler
LogHandlerFactory = Callable[[], Handler]
@@ -11,7 +10,6 @@
__all__ = ["LogHandlerFactory", "configure_logger", "setup_logging"]
_CONFIGURED_LOGGER_NAMES = (
- app_settings.app_name,
"uvicorn",
"uvicorn.access",
"uvicorn.error",
diff --git a/app/logger/formatters.py b/app/logger/formatters.py
deleted file mode 100644
index 0a4b2d2..0000000
--- a/app/logger/formatters.py
+++ /dev/null
@@ -1,117 +0,0 @@
-import json
-import logging
-import os
-import threading
-import time
-from logging import Formatter
-from typing import Any
-
-from app.settings import app_settings
-
-__all__ = ["JsonFormatter"]
-
-_STANDARD_LOG_RECORD_ATTRIBUTES = {
- "args",
- "asctime",
- "created",
- "exc_info",
- "exc_text",
- "filename",
- "funcName",
- "levelname",
- "levelno",
- "lineno",
- "message",
- "module",
- "msecs",
- "msg",
- "name",
- "pathname",
- "process",
- "processName",
- "relativeCreated",
- "stack_info",
- "thread",
- "threadName",
- "taskName",
- "_app_json_log_payload",
-}
-
-
-def _json_safe(value: Any) -> Any:
- if value is None or isinstance(value, str | int | float | bool):
- return value
-
- if isinstance(value, dict):
- return {str(key): _json_safe(item) for key, item in value.items()}
-
- if isinstance(value, list | tuple | set | frozenset):
- return [_json_safe(item) for item in value]
-
- return str(value)
-
-
-class JsonFormatter(Formatter):
- def _severity_number(self, level: int) -> int:
- if level >= logging.CRITICAL:
- return 21
- if level >= logging.ERROR:
- return 17
- if level >= logging.WARNING:
- return 13
- if level >= logging.INFO:
- return 9
- if level >= logging.DEBUG:
- return 5
- return 1
-
- def _attributes(self, record: logging.LogRecord) -> dict[str, Any]:
- attributes: dict[str, Any] = {
- "logger.name": record.name,
- "code.filepath": record.pathname,
- "code.function": record.funcName,
- "code.lineno": record.lineno,
- "process.pid": os.getpid(),
- "thread.id": threading.get_ident(),
- "thread.name": record.threadName,
- }
-
- attributes.update(
- {
- key: _json_safe(value)
- for key, value in record.__dict__.items()
- if key not in _STANDARD_LOG_RECORD_ATTRIBUTES
- }
- )
-
- if record.exc_info:
- exception = record.exc_info[1]
- attributes["exception.type"] = type(exception).__name__
- attributes["exception.message"] = str(exception)
- attributes["exception.stacktrace"] = self.formatException(record.exc_info)
-
- return attributes
-
- def format(self, record: logging.LogRecord) -> str:
- attributes = self._attributes(record)
- log_object: dict[str, Any] = {
- "time_unix_nano": int(record.created * 1_000_000_000),
- "observed_time_unix_nano": time.time_ns(),
- "severity_text": record.levelname,
- "severity_number": self._severity_number(record.levelno),
- "body": record.getMessage(),
- "resource": {
- "service.name": app_settings.app_name,
- },
- "scope": {
- "name": record.name,
- },
- "attributes": attributes,
- "timestamp": self.formatTime(record, self.datefmt),
- "name": record.name,
- "level": record.levelname,
- "message": record.getMessage(),
- }
- if record.exc_info:
- log_object["exception"] = attributes["exception.stacktrace"]
- return json.dumps(log_object)
diff --git a/app/logger/handlers.py b/app/logger/handlers.py
index 293a23e..114dcc0 100644
--- a/app/logger/handlers.py
+++ b/app/logger/handlers.py
@@ -1,69 +1,20 @@
import logging
-import sys
-from logging import Formatter, Handler
-from typing import TextIO
import logfire
from app.logger.filters import HealthEndpointFilter
-from app.logger.formatters import JsonFormatter
LOG_LEVEL = logging.INFO
-JSON_LOG_ATTRIBUTE = "app.log.json"
-_JSON_PAYLOAD_RECORD_ATTRIBUTE = "_app_json_log_payload"
__all__ = [
- "JSON_LOG_ATTRIBUTE",
"LOG_LEVEL",
- "JsonPayloadLogfireLoggingHandler",
- "formatter",
"get_logfire_handler",
]
-formatter: Formatter = JsonFormatter()
-
-
-def _json_payload(record: logging.LogRecord) -> str:
- payload = getattr(record, _JSON_PAYLOAD_RECORD_ATTRIBUTE, None)
- if isinstance(payload, str):
- return payload
-
- payload = formatter.format(record)
- setattr(record, _JSON_PAYLOAD_RECORD_ATTRIBUTE, payload)
- return payload
-
-
-class JsonPayloadLogfireLoggingHandler(logfire.LogfireLoggingHandler):
- terminator = "\n"
-
- def __init__(
- self,
- level: int | str = LOG_LEVEL,
- *,
- stream: TextIO | None = None,
- ) -> None:
- super().__init__(level=level, fallback=logging.NullHandler())
- self.stream = stream or sys.stdout
- self.addFilter(HealthEndpointFilter())
-
- def fill_attributes(self, record: logging.LogRecord) -> dict[str, object]:
- attributes = super().fill_attributes(record)
- attributes[JSON_LOG_ATTRIBUTE] = _json_payload(record)
- return attributes
-
- def emit(self, record: logging.LogRecord) -> None:
- try:
- payload = _json_payload(record)
- super().emit(record)
- self.stream.write(payload + self.terminator)
- self.flush()
- except Exception: # noqa: BLE001
- self.handleError(record)
-
- def flush(self) -> None:
- if hasattr(self.stream, "flush"):
- self.stream.flush()
-
-
-def get_logfire_handler() -> Handler:
- return JsonPayloadLogfireLoggingHandler(level=LOG_LEVEL)
+def get_logfire_handler() -> logfire.LogfireLoggingHandler:
+ handler = logfire.LogfireLoggingHandler(
+ level=LOG_LEVEL,
+ fallback=logging.NullHandler(),
+ )
+ handler.addFilter(HealthEndpointFilter())
+ return handler
diff --git a/app/logger/logger.py b/app/logger/logger.py
deleted file mode 100644
index 08ebf7b..0000000
--- a/app/logger/logger.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import logging
-from logging import Logger
-
-from app.logger.configuration import setup_logging as _setup_logging
-from app.logger.formatters import JsonFormatter
-from app.logger.handlers import LOG_LEVEL, formatter
-from app.settings import app_settings
-from app.telemetry import get_otel_log_handler
-
-__all__ = [
- "JsonFormatter",
- "debug",
- "error",
- "exception",
- "formatter",
- "info",
- "logger",
- "setup_logging",
- "warning",
-]
-
-logger: Logger = logging.getLogger(app_settings.app_name)
-logger.setLevel(LOG_LEVEL)
-
-
-def setup_logging() -> None:
- _setup_logging(otel_handler_factory=get_otel_log_handler)
-
-
-def info(msg: str, *args: object) -> None:
- logger.info(msg, *args)
-
-
-def error(msg: str, *args: object) -> None:
- logger.error(msg, *args)
-
-
-def exception(msg: str, *args: object, exc: BaseException) -> None:
- logger.error(
- msg,
- *args,
- exc_info=(type(exc), exc, exc.__traceback__),
- )
-
-
-def warning(msg: str, *args: object) -> None:
- logger.warning(msg, *args)
-
-
-def debug(msg: str, *args: object) -> None:
- logger.debug(msg, *args)
diff --git a/app/main.py b/app/main.py
index 58ca744..0f83f3e 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,6 +1,7 @@
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
+import logfire
import uvicorn
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
@@ -26,12 +27,16 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None]:
)
_app.openapi_schema = None
telemetry.instrument_sqlalchemy(get_engine())
- logger.info(f"Starting up {app_settings.app_name} on port {app_settings.port}")
+ logfire.info(
+ "Starting up {service_name} on port {port}",
+ service_name=app_settings.app_name,
+ port=app_settings.port,
+ )
try:
yield
finally:
await get_engine().dispose()
- logger.info("Application shutdown")
+ logfire.info("Application shutdown")
app = OIDCOpenAPIFastAPI(
diff --git a/app/telemetry.py b/app/telemetry.py
index 32dc1da..cca96bf 100644
--- a/app/telemetry.py
+++ b/app/telemetry.py
@@ -23,7 +23,7 @@ def configure_otel() -> None:
logfire.configure(
service_name=app_settings.app_name,
send_to_logfire="if-token-present",
- console=False,
+ console=logfire.ConsoleOptions(colors="never", show_project_link=False),
)
_configured = True
diff --git a/reports/open-github-issues-audit.html b/reports/open-github-issues-audit.html
index 833bb8a..54bfcac 100644
--- a/reports/open-github-issues-audit.html
+++ b/reports/open-github-issues-audit.html
@@ -5,11 +5,11 @@
Open GitHub Issues Audit — FastAPI Template
-Live issue audit · main @ 9e5b527 · updated 11 July 2026
GitHub issues: implementation status Evidence-based review of the 26 issues originally audited in Subhransu-De/FastAPI-Template . Seven completed issues have since been closed, leaving 19 open. “Partially done” means meaningful criteria are present; “Still to work on” means no substantive issue-level implementation was found.
+Live issue audit · main @ 6f0b6dc · updated 12 July 2026
GitHub issues: implementation status Evidence-based review of the 26 issues originally audited in Subhransu-De/FastAPI-Template . Seven completed issues have since been closed, leaving 19 open. “Partially done” means meaningful criteria or reusable prerequisites are present; actionable remaining lists exclude documentation-only work.
19 issues still open
7 closed as done
3 partially done
16 still to work on
All tracked 26 Closed as done 7 Partially done 3 Still to work on 16
Done
@@ -22,9 +22,9 @@
Reproducible local Keycloak
Done · closed
Committed realm exports are mounted by Compose, exact Swagger callbacks are configured automatically, and an isolated fresh-stack test verified the API, OIDC discovery, imported user, clients, migrations, and one-shot configuration. docs/local-keycloak.md documents identities, clients, reset behavior, internal/public URLs, and local-only quirks; docs/local-development.md explains the rest of the local stack.
Partially done
-Observability expectations
Partial
app/telemetry.py instruments FastAPI and SQLAlchemy, excludes health traffic, and JSON logging exists.
Remaining: trace/request correlation in logs, metrics strategy, env-var docs, and operator dashboard/alert guidance.
-Expand README into production documentation
Partial
README covers local/Docker setup, tests, migrations, OIDC basics, and the real CI badge.
Remaining: complete environment/secrets reference, production deployment/startup assumptions, health/observability details, and troubleshooting.
-Full ECS Fargate Terraform deployment
Partial
A generic API image, migration image, health endpoint, and brief ECS migration note are reusable prerequisites.
Remaining: all requested Terraform resources (networking, ECS, ALB, IAM, logs/secrets), outputs, and deployment documentation.
+Observability expectations
Implementation complete on branch
2 of 2 non-documentation criteria complete on issue-19-observability-correlation
Done / available foundation Application lifecycle and exception events use Logfire's direct structured logging API. The stock LogfireLoggingHandler bridges Uvicorn and third-party standard-library logs into the same telemetry pipeline without a project-owned handler subclass or duplicate JSON payload. Logfire automatically correlates logs emitted within active OpenTelemetry traces; focused tests verify shared trace context. app/telemetry.py instruments FastAPI and SQLAlchemy; request spans include route/status data plus filtered client and OIDC attributes.Sensitive request values are dropped, and unit/integration tests exercise telemetry behavior. /health spans and access logs are intentionally excluded and covered by tests, fully satisfying the noise-reduction criterion.Remaining implementation work None. Both non-documentation acceptance criteria are implemented on the issue branch.
+Expand README into production documentation
Documentation only
Excluded from the implementation backlog
Current foundation README covers installation, host and Docker Compose startup, tests, migrations, and the application architecture. Production image and migration-image responsibilities are documented, including the ECS/EKS pre-deployment migration flow. The CI/CD badge points to the existing workflow. Local Keycloak, Swagger PKCE, and browser-secret safety are introduced; detailed environment and local identity references exist under docs/. Remaining implementation work None. The open acceptance criteria for this issue are documentation-only.
+Full ECS Fargate Terraform deployment
Prerequisites only
0 of 7 non-documentation criteria complete
Done / reusable prerequisites A generic runtime Dockerfile and separate one-shot Dockerfile.migration are ready to publish. The application exposes GET /health for a future load-balancer target. Application configuration already uses environment variables. README briefly explains running the migration image as an ECS one-off task before service rollout. Remaining implementation work Create infra/ and Terraform for VPC networking and security groups. Provision the ECS cluster, Fargate task definition, and ECS service. Provision the ALB, target group, listener, and working health check; verify the API through the ALB. Wire application configuration and secrets through Secrets Manager or SSM Parameter Store. Configure a CloudWatch log group and ECS task logging. Add least-privilege task execution and application task IAM roles. Output the ALB DNS name, cluster name, service name, and log-group name.
Still to be worked on
Production server and reverse-proxy guidance
Still to work on
Only basic host/port/reload Uvicorn settings exist. Worker/replica strategy, trusted forwarded headers, HTTPS termination, graceful shutdown, and timeout guidance are absent.
@@ -44,5 +44,5 @@
Elastic Beanstalk Docker Terraform
Still to work on
No EB Terraform, source bundle/Dockerrun setup, configurable environment, outputs, or deployment/verification guide exists.
Actuator-style health and build information
Still to work on
Only lowercase {"status":"up"} at /health exists. All four Actuator routes, readiness dependency checks, package metadata, OCI build provenance, policies, tests, and docs remain absent.
-Method: fetched issues with GitHub CLI, checked each acceptance criterion against the current default branch, and cited repository evidence. Closed as completed: #11, #12, #13, #16, and #57. Classification reflects repository state—not unmerged branches or external deployments.
-