From d66514392a63b2a0010a2c9d613c811fda5a6ddf Mon Sep 17 00:00:00 2001 From: Stephen Kilbourn Date: Mon, 15 Sep 2025 09:09:16 -0600 Subject: [PATCH 1/7] fix: add tileMatrixSetId to path params (#523) --- .github/workflows/tests/test_stac.py | 4 +++- stac_api/runtime/src/extension.py | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests/test_stac.py b/.github/workflows/tests/test_stac.py index 2b50fe79..cb2eed5e 100644 --- a/.github/workflows/tests/test_stac.py +++ b/.github/workflows/tests/test_stac.py @@ -141,9 +141,11 @@ def test_stac_api(self): def test_stac_to_raster(self): """test link to raster api.""" + tile_matrix_set_id = "WebMercatorQuad" + # tilejson resp = httpx.get( - f"{self.stac_endpoint}/{self.collections_route}/{self.seeded_collection}/items/{self.seeded_id}/tilejson.json", + f"{self.stac_endpoint}/{self.collections_route}/{self.seeded_collection}/items/{self.seeded_id}/{tile_matrix_set_id}/tilejson.json", params={"assets": "cog"}, ) assert resp.status_code == 307 diff --git a/stac_api/runtime/src/extension.py b/stac_api/runtime/src/extension.py index c9ad256f..62bbe3ba 100644 --- a/stac_api/runtime/src/extension.py +++ b/stac_api/runtime/src/extension.py @@ -34,12 +34,15 @@ def register(self, app: FastAPI, titiler_endpoint: str) -> None: @tracer.capture_method @router.get( - "/collections/{collectionId}/items/{itemId}/tilejson.json", + "/collections/{collectionId}/items/{itemId}/{tileMatrixSetId}/tilejson.json", ) async def tilejson( request: Request, collectionId: str = Path(..., description="Collection ID"), itemId: str = Path(..., description="Item ID"), + tileMatrixSetId: str = Path( + ..., description="TileMatrixSet name (e.g. WebMercatorQuad)." + ), tile_format: Optional[str] = Query( None, description="Output image type. Default is auto." ), @@ -91,7 +94,7 @@ async def tilejson( qs.append(("collection", collectionId)) return RedirectResponse( - f"{titiler_endpoint}/stac/tilejson.json?{urlencode(qs)}" + f"{titiler_endpoint}/stac/{tileMatrixSetId}/tilejson.json?{urlencode(qs)}" ) @tracer.capture_method From e57e66bda835608934e5d7d1e911589c5dd1fd71 Mon Sep 17 00:00:00 2001 From: Stephen Kilbourn Date: Mon, 15 Sep 2025 13:13:43 -0600 Subject: [PATCH 2/7] fix: update parse_obj to model_validate (#511) --- ingest_api/runtime/src/services.py | 7 ++++--- ingest_api/runtime/tests/conftest.py | 2 +- scripts/run-local-tests.sh | 8 ++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ingest_api/runtime/src/services.py b/ingest_api/runtime/src/services.py index f4cc2b05..ed6278ee 100644 --- a/ingest_api/runtime/src/services.py +++ b/ingest_api/runtime/src/services.py @@ -4,7 +4,7 @@ import src.schemas as schemas from boto3.dynamodb import conditions from boto3.dynamodb.types import DYNAMODB_CONTEXT -from pydantic import parse_obj_as +from pydantic import TypeAdapter if TYPE_CHECKING: from mypy_boto3_dynamodb.service_resource import Table @@ -25,7 +25,7 @@ def fetch_one(self, username: str, ingestion_id: str): Key={"created_by": username, "id": ingestion_id}, ) try: - return schemas.Ingestion.parse_obj(response["Item"]) + return schemas.Ingestion.model_validate(response["Item"]) except KeyError: raise NotInDb("Record not found") @@ -38,8 +38,9 @@ def fetch_many( **{"Limit": limit} if limit else {}, **{"ExclusiveStartKey": next} if next else {}, ) + list_of_ingestions = TypeAdapter(List[schemas.Ingestion]) return { - "items": parse_obj_as(List[schemas.Ingestion], response["Items"]), + "items": list_of_ingestions.validate_python(response["Items"]), "next": response.get("LastEvaluatedKey"), } diff --git a/ingest_api/runtime/tests/conftest.py b/ingest_api/runtime/tests/conftest.py index ef2ead8c..f5475834 100644 --- a/ingest_api/runtime/tests/conftest.py +++ b/ingest_api/runtime/tests/conftest.py @@ -162,6 +162,6 @@ def example_ingestion(example_stac_item): id=example_stac_item["id"], created_by="test-user", status=schemas.Status.queued, - item=Item.parse_obj(example_stac_item), + item=Item.model_validate(example_stac_item), created_at=datetime.now(), ) diff --git a/scripts/run-local-tests.sh b/scripts/run-local-tests.sh index 62c6ce1e..14d3b7a0 100755 --- a/scripts/run-local-tests.sh +++ b/scripts/run-local-tests.sh @@ -1,6 +1,14 @@ #!/bin/bash set -e +# ================================================================= +# Ensure all Python dependencies are installed +# ================================================================= +echo "--- Installing all development dependencies ---" +pip install -r ingest_api/runtime/requirements_dev.txt +echo "--- Dependency installation complete ---" +# ================================================================= + # Lint pre-commit run --all-files From f6fc5be3d7faedd89116e3be6f4153c4ca2e6b07 Mon Sep 17 00:00:00 2001 From: Alexandra Kirk Date: Thu, 18 Sep 2025 15:08:51 -0600 Subject: [PATCH 3/7] fix: upgrade pystac version and implement override for a lower stac spec version in configuration --- stac_api/infrastructure/config.py | 4 ++++ stac_api/infrastructure/construct.py | 1 + stac_api/runtime/setup.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/stac_api/infrastructure/config.py b/stac_api/infrastructure/config.py index a680fe07..3fde8a7b 100644 --- a/stac_api/infrastructure/config.py +++ b/stac_api/infrastructure/config.py @@ -58,6 +58,10 @@ class vedaSTACSettings(BaseSettings): False, description="Boolean to disable default API gateway endpoints for stac, raster, and ingest APIs. Defaults to false.", ) + pystac_stac_version_override: Optional[str] = Field( + "1.0.0", + description="Stac version override for Pystac validations https://pystac.readthedocs.io/en/stable/api/version.html", + ) @model_validator(mode="before") def check_transaction_fields(cls, values): diff --git a/stac_api/infrastructure/construct.py b/stac_api/infrastructure/construct.py index fed1937e..efdfe3c7 100644 --- a/stac_api/infrastructure/construct.py +++ b/stac_api/infrastructure/construct.py @@ -53,6 +53,7 @@ def __init__( ), "DB_MIN_CONN_SIZE": "0", "DB_MAX_CONN_SIZE": "1", + "PYSTAC_STAC_VERSION_OVERRIDE": veda_stac_settings.pystac_stac_version_override, **{k.upper(): v for k, v in veda_stac_settings.env.items()}, } diff --git a/stac_api/runtime/setup.py b/stac_api/runtime/setup.py index feb03507..4959efba 100644 --- a/stac_api/runtime/setup.py +++ b/stac_api/runtime/setup.py @@ -19,7 +19,7 @@ "pygeoif<=0.8", # newest release (1.0+ / 09-22-2022) breaks a number of other geo libs "aws-lambda-powertools>=1.18.0", "aws_xray_sdk>=2.6.0,<3", - "pystac[validation]==1.10.1", + "pystac[validation]>=1.14.0", "pydantic>2", "eoapi-auth-utils==0.3.0", ] From f9b470daa6906af5b9455cff0a0372d57ce6d9b9 Mon Sep 17 00:00:00 2001 From: ividito Date: Wed, 10 Sep 2025 11:11:15 -0700 Subject: [PATCH 4/7] feat: Switch to middleware for monitoring --- ingest_api/runtime/src/main.py | 7 +- ingest_api/runtime/src/monitoring.py | 190 +++++++++++++++++++++------ raster_api/runtime/src/app.py | 14 +- raster_api/runtime/src/monitoring.py | 188 ++++++++++++++++++++------ stac_api/runtime/src/app.py | 8 +- stac_api/runtime/src/monitoring.py | 187 ++++++++++++++++++++------ 6 files changed, 455 insertions(+), 139 deletions(-) diff --git a/ingest_api/runtime/src/main.py b/ingest_api/runtime/src/main.py index a3940359..4a6c3d17 100644 --- a/ingest_api/runtime/src/main.py +++ b/ingest_api/runtime/src/main.py @@ -6,7 +6,7 @@ from src.collection_publisher import CollectionPublisher, ItemPublisher from src.config import settings from src.doc import DESCRIPTION -from src.monitoring import LoggerRouteHandler, logger, metrics, tracer +from src.monitoring import ObservabilityMiddleware, logger, metrics, tracer from fastapi import Depends, FastAPI, HTTPException, Security from fastapi.exceptions import RequestValidationError @@ -32,8 +32,6 @@ }, ) -app.router.route_class = LoggerRouteHandler - collection_publisher = CollectionPublisher() item_publisher = ItemPublisher() @@ -217,6 +215,9 @@ def who_am_i(claims=Depends(oidc_auth.valid_token_dependency)): return claims +app.add_middleware(ObservabilityMiddleware) + + # If the correlation header is used in the UI, we can analyze traces that originate from a given user or client @app.middleware("http") async def add_correlation_id(request: Request, call_next): diff --git a/ingest_api/runtime/src/monitoring.py b/ingest_api/runtime/src/monitoring.py index 2eec20ee..122ed4c6 100644 --- a/ingest_api/runtime/src/monitoring.py +++ b/ingest_api/runtime/src/monitoring.py @@ -1,13 +1,13 @@ -"""Observability utils""" +"""Observability middleware for logging and tracing requests.""" import json -from typing import Callable +import time +from typing import Callable, Optional from aws_lambda_powertools import Logger, Metrics, Tracer, single_metric -from aws_lambda_powertools.metrics import MetricUnit # noqa: F401 -from src.config import settings +from aws_lambda_powertools.metrics import MetricUnit +from src.config import ApiSettings -from fastapi import Request, Response -from fastapi.routing import APIRoute +settings = ApiSettings() logger: Logger = Logger(service="ingest-api", namespace="veda-backend") metrics: Metrics = Metrics(namespace="veda-backend") @@ -15,43 +15,151 @@ tracer: Tracer = Tracer() -class LoggerRouteHandler(APIRoute): - """Extension of base APIRoute to add context to log statements, as well as record usage metrics""" +class ObservabilityMiddleware: + """Observability middleware for logging and tracing requests.""" - def get_route_handler(self) -> Callable: - """Overide route handler method to add logs, metrics, tracing""" - original_route_handler = super().get_route_handler() + def __init__(self, app: Callable): + """Observability middleware for logging and tracing requests.""" + self.app = app - async def route_handler(request: Request) -> Response: - # Add fastapi context to logs - body = await request.body() + async def __call__(self, scope, receive, send): # noqa: C901 + """Observability middleware for logging and tracing requests.""" + # Only handle HTTP requests + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + method: str = scope.get("method", "GET") + raw_path: str = scope.get("path", "") + + # --- Buffer the incoming body so we can log it but still pass it downstream --- + body = b"" + more_body = True + + # Consume the body from the original receive channel + while more_body: + message = await receive() + if message["type"] != "http.request": + # Pass through non-http.request messages + await self.app(scope, _make_receive_replay([message]), send) + return + body += message.get("body", b"") + more_body = message.get("more_body", False) + + # Prepare a receive wrapper that replays the buffered body to the app + receive_replayed = _make_receive_replay( + [{"type": "http.request", "body": body, "more_body": False}] + ) + + # Try to parse JSON body for structured logging (non-JSON becomes None) + body_json = None + if body: try: body_json = json.loads(body) - except json.decoder.JSONDecodeError: + except json.JSONDecodeError: body_json = None - ctx = { - "path": request.url.path, - "path_params": request.path_params, - "body": body_json, - "route": self.path, - "method": request.method, - } - logger.append_keys(fastapi=ctx) - logger.info("Received request") - - with single_metric( - name="RequestCount", - unit=MetricUnit.Count, - value=1, - default_dimensions=metrics.default_dimensions, - namespace="veda-backend", - ) as metric: - metric.add_dimension( - name="route", value=f"{request.method} {self.path}" - ) - - tracer.put_annotation(key="path", value=request.url.path) - tracer.capture_method(original_route_handler)(request) - return await original_route_handler(request) - - return route_handler + + # Initial context logging (route template not yet resolved here) + ctx = { + "path": raw_path, + "method": method, + "path_params": None, # will try to resolve after routing + "route": None, # will try to resolve after routing + "body": body_json, + } + logger.append_keys(fastapi=ctx) + logger.info("Received request") + + # Add X-Ray annotations early + tracer.put_annotation(key="path", value=raw_path) + tracer.put_annotation(key="method", value=method) + + # Capture status code via send wrapper + status_holder = {"status": 500} + resp_size_holder = {"bytes": 0} + + start = time.perf_counter_ns() + + async def send_wrapper(message): + if message["type"] == "http.response.start": + status_holder["status"] = message.get("status", 500) + # If Content-Length is set, remember it; otherwise we’ll sum body chunks + for (h, v) in message.get("headers", []) or []: + if h.lower() == b"content-length": + try: + resp_size_holder["bytes"] = int(v.decode("latin1")) + except Exception: + pass + elif message["type"] == "http.response.body": + # If no Content-Length, accumulate bytes + if resp_size_holder["bytes"] == 0: + resp_size_holder["bytes"] += len(message.get("body") or b"") + await send(message) + + # Route/execute the request with tracing around the app call + @tracer.capture_method(capture_response=False) + async def _call_downstream(): + await self.app(scope, receive_replayed, send_wrapper) + + await _call_downstream() + + elapsed_ms = (time.perf_counter_ns() - start) / 1_000_000.0 + status = status_holder["status"] + status_family = f"{status // 100}xx" + + # After downstream handled routing, try to resolve route template & path params + route_template: Optional[str] = None + path_params = None + route_obj = scope.get("route") + if route_obj is not None: + # FastAPI/Starlette exposes a path_format like "/items/{item_id}" + route_template = getattr(route_obj, "path_format", None) or getattr( + route_obj, "path", None + ) + path_params = scope.get("path_params", None) + + # Update log context with resolved info and status + final_ctx = { + "path": raw_path, + "method": method, + "path_params": path_params, + "route": route_template or raw_path, + "status_code": status_holder["status"], + "body": body_json, + } + logger.append_keys(fastapi=final_ctx) + logger.info("Completed request") + + with single_metric( + name="http_requests_total", + unit=MetricUnit.Count, + value=1, + default_dimensions=metrics.default_dimensions, + namespace="veda-backend", + ) as m: + m.add_dimension("route_template", route_template) + m.add_dimension("status_family", status_family) + m.add_metric("http_requests_total", 1, MetricUnit.Count) + m.add_metric( + "http_request_duration_ms", elapsed_ms, MetricUnit.Milliseconds + ) + m.add_metric( + "http_response_size_bytes", resp_size_holder["bytes"], MetricUnit.Bytes + ) + + +def _make_receive_replay(messages): + """ + Build a 'receive' callable that replays given ASGI messages once. + """ + + async def _receive(): + if _receive._idx < len(messages): + msg = messages[_receive._idx] + _receive._idx += 1 + return msg + # No more data + return {"type": "http.request", "body": b"", "more_body": False} + + _receive._idx = 0 # type: ignore[attr-defined] + return _receive diff --git a/raster_api/runtime/src/app.py b/raster_api/runtime/src/app.py index 588f4604..b1f3e460 100644 --- a/raster_api/runtime/src/app.py +++ b/raster_api/runtime/src/app.py @@ -8,10 +8,10 @@ from src.config import ApiSettings from src.dependencies import ColorMapParams, cmap from src.extensions import stacViewerExtension -from src.monitoring import LoggerRouteHandler, logger, metrics, tracer +from src.monitoring import ObservabilityMiddleware, logger, metrics, tracer from src.version import __version__ as veda_raster_version -from fastapi import APIRouter, FastAPI +from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware from starlette.requests import Request from starlette_cramjam.middleware import CompressionMiddleware @@ -69,8 +69,6 @@ async def lifespan(app: FastAPI): root_path=settings.root_path, ) -# router to be applied to all titiler route factories (improves logs with FastAPI context) -router = APIRouter(route_class=LoggerRouteHandler) add_exception_handlers(app, DEFAULT_STATUS_CODES) add_exception_handlers(app, MOSAIC_STATUS_CODES) @@ -82,7 +80,6 @@ async def lifespan(app: FastAPI): path_dependency=SearchIdParams, environment_dependency=settings.get_gdal_config, process_dependency=PostProcessParams, - router=APIRouter(route_class=LoggerRouteHandler), # add /statistics [POST] (default to False) add_statistics=True, # add /map viewer (default to False) @@ -150,7 +147,6 @@ async def lifespan(app: FastAPI): path_dependency=ItemIdParams, router_prefix="/collections/{collection_id}/items/{item_id}", environment_dependency=settings.get_gdal_config, - router=APIRouter(route_class=LoggerRouteHandler), extensions=[ stacViewerExtension(), ], @@ -170,7 +166,6 @@ async def lifespan(app: FastAPI): path_dependency=ItemIdParams, router_prefix="/alt/collections/{collection_id}/items/{item_id}", environment_dependency=settings.get_gdal_config, - router=APIRouter(route_class=LoggerRouteHandler), extensions=[ stacViewerExtension(), ], @@ -189,7 +184,6 @@ async def lifespan(app: FastAPI): cog = TilerFactory( router_prefix="/cog", environment_dependency=settings.get_gdal_config, - router=APIRouter(route_class=LoggerRouteHandler), extensions=[ cogValidateExtension(), cogViewerExtension(), @@ -244,6 +238,8 @@ def ping(): }, ) +app.add_middleware(ObservabilityMiddleware) + # If the correlation header is used in the UI, we can analyze traces that originate from a given user or client @app.middleware("http") @@ -276,5 +272,5 @@ async def add_correlation_id(request: Request, call_next): async def validation_exception_handler(request, err): """Handle exceptions that aren't caught elsewhere""" metrics.add_metric(name="UnhandledExceptions", unit=MetricUnit.Count, value=1) - logger.exception("Unhandled exception") + logger.exception("Unhandled exception:", err) return JSONResponse(status_code=500, content={"detail": "Internal Server Error"}) diff --git a/raster_api/runtime/src/monitoring.py b/raster_api/runtime/src/monitoring.py index 7237806d..72d57cd1 100644 --- a/raster_api/runtime/src/monitoring.py +++ b/raster_api/runtime/src/monitoring.py @@ -1,14 +1,12 @@ -"""Observability utils""" +"""Observability middleware for logging and tracing requests.""" import json -from typing import Callable +import time +from typing import Callable, Optional from aws_lambda_powertools import Logger, Metrics, Tracer, single_metric -from aws_lambda_powertools.metrics import MetricUnit # noqa: F401 +from aws_lambda_powertools.metrics import MetricUnit from src.config import ApiSettings -from fastapi import Request, Response -from fastapi.routing import APIRoute - settings = ApiSettings() logger: Logger = Logger(service="raster-api", namespace="veda-backend") @@ -17,45 +15,151 @@ tracer: Tracer = Tracer() -class LoggerRouteHandler(APIRoute): - """Extension of base APIRoute to add context to log statements, as well as record usage metrics""" +class ObservabilityMiddleware: + """Observability middleware for logging and tracing requests.""" + + def __init__(self, app: Callable): + """Observability middleware for logging and tracing requests.""" + self.app = app + + async def __call__(self, scope, receive, send): # noqa: C901 + """Observability middleware for logging and tracing requests.""" + # Only handle HTTP requests + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + method: str = scope.get("method", "GET") + raw_path: str = scope.get("path", "") + + # --- Buffer the incoming body so we can log it but still pass it downstream --- + body = b"" + more_body = True - def get_route_handler(self) -> Callable: - """Overide route handler method to add logs, metrics, tracing""" - original_route_handler = super().get_route_handler() + # Consume the body from the original receive channel + while more_body: + message = await receive() + if message["type"] != "http.request": + # Pass through non-http.request messages + await self.app(scope, _make_receive_replay([message]), send) + return + body += message.get("body", b"") + more_body = message.get("more_body", False) - async def route_handler(request: Request) -> Response: - # Add fastapi context to logs - body = await request.body() + # Prepare a receive wrapper that replays the buffered body to the app + receive_replayed = _make_receive_replay( + [{"type": "http.request", "body": body, "more_body": False}] + ) + + # Try to parse JSON body for structured logging (non-JSON becomes None) + body_json = None + if body: try: body_json = json.loads(body) - except json.decoder.JSONDecodeError: + except json.JSONDecodeError: body_json = None - ctx = { - "path": request.url.path, - "path_params": request.path_params, - "body": body_json, - "route": self.path, - "method": request.method, - } - logger.append_keys(fastapi=ctx) - logger.info("Received request") - - with single_metric( - name="RequestCount", - unit=MetricUnit.Count, - value=1, - default_dimensions=metrics.default_dimensions, - namespace="veda-backend", - ) as metric: - metric.add_dimension( - name="route", value=f"{request.method} {self.path}" - ) - - tracer.put_annotation(key="path", value=request.url.path) - tracer.capture_method(original_route_handler)(request) - - return await original_route_handler(request) - - return route_handler + # Initial context logging (route template not yet resolved here) + ctx = { + "path": raw_path, + "method": method, + "path_params": None, # will try to resolve after routing + "route": None, # will try to resolve after routing + "body": body_json, + } + logger.append_keys(fastapi=ctx) + logger.info("Received request") + + # Add X-Ray annotations early + tracer.put_annotation(key="path", value=raw_path) + tracer.put_annotation(key="method", value=method) + + # Capture status code via send wrapper + status_holder = {"status": 500} + resp_size_holder = {"bytes": 0} + + start = time.perf_counter_ns() + + async def send_wrapper(message): + if message["type"] == "http.response.start": + status_holder["status"] = message.get("status", 500) + # If Content-Length is set, remember it; otherwise we’ll sum body chunks + for (h, v) in message.get("headers", []) or []: + if h.lower() == b"content-length": + try: + resp_size_holder["bytes"] = int(v.decode("latin1")) + except Exception: + pass + elif message["type"] == "http.response.body": + # If no Content-Length, accumulate bytes + if resp_size_holder["bytes"] == 0: + resp_size_holder["bytes"] += len(message.get("body") or b"") + await send(message) + + # Route/execute the request with tracing around the app call + @tracer.capture_method(capture_response=False) + async def _call_downstream(): + await self.app(scope, receive_replayed, send_wrapper) + + await _call_downstream() + + elapsed_ms = (time.perf_counter_ns() - start) / 1_000_000.0 + status = status_holder["status"] + status_family = f"{status // 100}xx" + + # After downstream handled routing, try to resolve route template & path params + route_template: Optional[str] = None + path_params = None + route_obj = scope.get("route") + if route_obj is not None: + # FastAPI/Starlette exposes a path_format like "/items/{item_id}" + route_template = getattr(route_obj, "path_format", None) or getattr( + route_obj, "path", None + ) + path_params = scope.get("path_params", None) + + # Update log context with resolved info and status + final_ctx = { + "path": raw_path, + "method": method, + "path_params": path_params, + "route": route_template or raw_path, + "status_code": status_holder["status"], + "body": body_json, + } + logger.append_keys(fastapi=final_ctx) + logger.info("Completed request") + + with single_metric( + name="http_requests_total", + unit=MetricUnit.Count, + value=1, + default_dimensions=metrics.default_dimensions, + namespace="veda-backend", + ) as m: + m.add_dimension("route_template", route_template) + m.add_dimension("status_family", status_family) + m.add_metric("http_requests_total", 1, MetricUnit.Count) + m.add_metric( + "http_request_duration_ms", elapsed_ms, MetricUnit.Milliseconds + ) + m.add_metric( + "http_response_size_bytes", resp_size_holder["bytes"], MetricUnit.Bytes + ) + + +def _make_receive_replay(messages): + """ + Build a 'receive' callable that replays given ASGI messages once. + """ + + async def _receive(): + if _receive._idx < len(messages): + msg = messages[_receive._idx] + _receive._idx += 1 + return msg + # No more data + return {"type": "http.request", "body": b"", "more_body": False} + + _receive._idx = 0 # type: ignore[attr-defined] + return _receive diff --git a/stac_api/runtime/src/app.py b/stac_api/runtime/src/app.py index 6a65cf74..c8cecb5f 100644 --- a/stac_api/runtime/src/app.py +++ b/stac_api/runtime/src/app.py @@ -16,7 +16,7 @@ ) from src.extension import TiTilerExtension -from fastapi import APIRouter, FastAPI +from fastapi import FastAPI from fastapi.responses import ORJSONResponse from stac_fastapi.api.app import StacApi from stac_fastapi.pgstac.db import close_db_connection, connect_to_db @@ -28,7 +28,7 @@ from starlette_cramjam.middleware import CompressionMiddleware from .core import VedaCrudClient -from .monitoring import LoggerRouteHandler, logger, metrics, tracer +from .monitoring import ObservabilityMiddleware, logger, metrics, tracer from .validation import ValidationMiddleware from eoapi.auth_utils import OpenIdConnectAuth, OpenIdConnectSettings @@ -83,7 +83,6 @@ async def lifespan(app: FastAPI): items_get_request_model=items_get_request_model, response_class=ORJSONResponse, middlewares=[Middleware(CompressionMiddleware), Middleware(ValidationMiddleware)], - router=APIRouter(route_class=LoggerRouteHandler), ) app = api.app @@ -143,6 +142,9 @@ async def viewer_page(request: Request): ) +app.add_middleware(ObservabilityMiddleware) + + # If the correlation header is used in the UI, we can analyze traces that originate from a given user or client @app.middleware("http") async def add_correlation_id(request: Request, call_next): diff --git a/stac_api/runtime/src/monitoring.py b/stac_api/runtime/src/monitoring.py index 1e3b3d61..eaf30f59 100644 --- a/stac_api/runtime/src/monitoring.py +++ b/stac_api/runtime/src/monitoring.py @@ -1,14 +1,12 @@ -"""Observability utils""" +"""Observability middleware for logging and tracing requests.""" import json -from typing import Callable +import time +from typing import Callable, Optional from aws_lambda_powertools import Logger, Metrics, Tracer, single_metric -from aws_lambda_powertools.metrics import MetricUnit # noqa: F401 +from aws_lambda_powertools.metrics import MetricUnit from src.config import ApiSettings -from fastapi import Request, Response -from fastapi.routing import APIRoute - settings = ApiSettings() logger: Logger = Logger(service="stac-api", namespace="veda-backend") @@ -17,44 +15,151 @@ tracer: Tracer = Tracer() -class LoggerRouteHandler(APIRoute): - """Extension of base APIRoute to add context to log statements, as well as record usage metrics""" +class ObservabilityMiddleware: + """Observability middleware for logging and tracing requests.""" + + def __init__(self, app: Callable): + """Observability middleware for logging and tracing requests.""" + self.app = app + + async def __call__(self, scope, receive, send): # noqa: C901 + """Observability middleware for logging and tracing requests.""" + # Only handle HTTP requests + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + method: str = scope.get("method", "GET") + raw_path: str = scope.get("path", "") + + # --- Buffer the incoming body so we can log it but still pass it downstream --- + body = b"" + more_body = True - def get_route_handler(self) -> Callable: - """Overide route handler method to add logs, metrics, tracing""" - original_route_handler = super().get_route_handler() + # Consume the body from the original receive channel + while more_body: + message = await receive() + if message["type"] != "http.request": + # Pass through non-http.request messages + await self.app(scope, _make_receive_replay([message]), send) + return + body += message.get("body", b"") + more_body = message.get("more_body", False) - async def route_handler(request: Request) -> Response: - # Add fastapi context to logs - body = await request.body() + # Prepare a receive wrapper that replays the buffered body to the app + receive_replayed = _make_receive_replay( + [{"type": "http.request", "body": body, "more_body": False}] + ) + + # Try to parse JSON body for structured logging (non-JSON becomes None) + body_json = None + if body: try: body_json = json.loads(body) - except json.decoder.JSONDecodeError: + except json.JSONDecodeError: body_json = None - ctx = { - "path": request.url.path, - "path_params": request.path_params, - "body": body_json, - "route": self.path, - "method": request.method, - } - logger.append_keys(fastapi=ctx) - logger.info("Received request") - - with single_metric( - name="RequestCount", - unit=MetricUnit.Count, - value=1, - default_dimensions=metrics.default_dimensions, - namespace="veda-backend", - ) as metric: - metric.add_dimension( - name="route", value=f"{request.method} {self.path}" - ) - - tracer.put_annotation(key="path", value=request.url.path) - tracer.capture_method(original_route_handler)(request) - return await original_route_handler(request) - - return route_handler + # Initial context logging (route template not yet resolved here) + ctx = { + "path": raw_path, + "method": method, + "path_params": None, # will try to resolve after routing + "route": None, # will try to resolve after routing + "body": body_json, + } + logger.append_keys(fastapi=ctx) + logger.info("Received request") + + # Add X-Ray annotations early + tracer.put_annotation(key="path", value=raw_path) + tracer.put_annotation(key="method", value=method) + + # Capture status code via send wrapper + status_holder = {"status": 500} + resp_size_holder = {"bytes": 0} + + start = time.perf_counter_ns() + + async def send_wrapper(message): + if message["type"] == "http.response.start": + status_holder["status"] = message.get("status", 500) + # If Content-Length is set, remember it; otherwise we’ll sum body chunks + for (h, v) in message.get("headers", []) or []: + if h.lower() == b"content-length": + try: + resp_size_holder["bytes"] = int(v.decode("latin1")) + except Exception: + pass + elif message["type"] == "http.response.body": + # If no Content-Length, accumulate bytes + if resp_size_holder["bytes"] == 0: + resp_size_holder["bytes"] += len(message.get("body") or b"") + await send(message) + + # Route/execute the request with tracing around the app call + @tracer.capture_method(capture_response=False) + async def _call_downstream(): + await self.app(scope, receive_replayed, send_wrapper) + + await _call_downstream() + + elapsed_ms = (time.perf_counter_ns() - start) / 1_000_000.0 + status = status_holder["status"] + status_family = f"{status // 100}xx" + + # After downstream handled routing, try to resolve route template & path params + route_template: Optional[str] = None + path_params = None + route_obj = scope.get("route") + if route_obj is not None: + # FastAPI/Starlette exposes a path_format like "/items/{item_id}" + route_template = getattr(route_obj, "path_format", None) or getattr( + route_obj, "path", None + ) + path_params = scope.get("path_params", None) + + # Update log context with resolved info and status + final_ctx = { + "path": raw_path, + "method": method, + "path_params": path_params, + "route": route_template or raw_path, + "status_code": status_holder["status"], + "body": body_json, + } + logger.append_keys(fastapi=final_ctx) + logger.info("Completed request") + + with single_metric( + name="http_requests_total", + unit=MetricUnit.Count, + value=1, + default_dimensions=metrics.default_dimensions, + namespace="veda-backend", + ) as m: + m.add_dimension("route_template", route_template) + m.add_dimension("status_family", status_family) + m.add_metric("http_requests_total", 1, MetricUnit.Count) + m.add_metric( + "http_request_duration_ms", elapsed_ms, MetricUnit.Milliseconds + ) + m.add_metric( + "http_response_size_bytes", resp_size_holder["bytes"], MetricUnit.Bytes + ) + + +def _make_receive_replay(messages): + """ + Build a 'receive' callable that replays given ASGI messages once. + """ + + async def _receive(): + if _receive._idx < len(messages): + msg = messages[_receive._idx] + _receive._idx += 1 + return msg + # No more data + return {"type": "http.request", "body": b"", "more_body": False} + + _receive._idx = 0 # type: ignore[attr-defined] + return _receive From 45eb69b7c420968974101dba402e98d69454cf24 Mon Sep 17 00:00:00 2001 From: ividito Date: Mon, 15 Sep 2025 09:17:21 -0700 Subject: [PATCH 5/7] fix: add additional metrics, add version metric --- app.py | 13 +++++----- ingest_api/infrastructure/config.py | 6 +++++ ingest_api/infrastructure/construct.py | 2 ++ ingest_api/runtime/src/config.py | 1 + ingest_api/runtime/src/monitoring.py | 34 +++++++++++++------------- raster_api/infrastructure/config.py | 6 +++++ raster_api/runtime/src/config.py | 1 + raster_api/runtime/src/monitoring.py | 34 +++++++++++++------------- stac_api/infrastructure/config.py | 6 +++++ stac_api/infrastructure/construct.py | 1 + stac_api/runtime/src/config.py | 1 + stac_api/runtime/src/monitoring.py | 34 +++++++++++++------------- 12 files changed, 82 insertions(+), 57 deletions(-) diff --git a/app.py b/app.py index f7ab5cdd..d235b8a1 100644 --- a/app.py +++ b/app.py @@ -45,6 +45,12 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: Aspects.of(self).add(PermissionsBoundaryAspect(permissions_boundary_policy)) +git_sha = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip() +try: + git_tag = subprocess.check_output(["git", "describe", "--tags"]).decode().strip() +except subprocess.CalledProcessError: + git_tag = "no-tag" + veda_stack = VedaStack( app, f"{veda_app_settings.app_name}-{veda_app_settings.stage_name()}", @@ -110,6 +116,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: stac_db_security_group_id=db_security_group.security_group_id, stac_api_url=stac_api.stac_api.url, raster_api_url=raster_api.raster_api.url, + git_sha=git_sha, ) ingest_api = ingest_api_construct( @@ -129,12 +136,6 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: db_vpc=vpc.vpc, ) -git_sha = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip() -try: - git_tag = subprocess.check_output(["git", "describe", "--tags"]).decode().strip() -except subprocess.CalledProcessError: - git_tag = "no-tag" - for key, value in { "Project": veda_app_settings.app_name, "Stack": veda_app_settings.stage_name(), diff --git a/ingest_api/infrastructure/config.py b/ingest_api/infrastructure/config.py index c15849d2..b01a6e6e 100644 --- a/ingest_api/infrastructure/config.py +++ b/ingest_api/infrastructure/config.py @@ -1,3 +1,4 @@ +import subprocess from getpass import getuser from typing import List, Optional @@ -93,6 +94,11 @@ class IngestorConfig(BaseSettings): case_sensitive=False, env_file=".env", env_prefix="VEDA_", extra="ignore" ) + git_sha: Optional[str] = Field( + subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode("utf-8"), + description="Git SHA of the current commit, used to track deployment version", + ) + @property def stack_name(self) -> str: return f"veda-stac-ingestion-{self.stage}" diff --git a/ingest_api/infrastructure/construct.py b/ingest_api/infrastructure/construct.py index e8cdb44f..5756a552 100644 --- a/ingest_api/infrastructure/construct.py +++ b/ingest_api/infrastructure/construct.py @@ -44,6 +44,7 @@ def __init__( "STAGE": config.stage, "CLIENT_ID": config.keycloak_ingest_api_client_id, "OPENID_CONFIGURATION_URL": str(config.openid_configuration_url), + "GIT_SHA": config.git_sha, } build_api_lambda_params = { @@ -236,6 +237,7 @@ def __init__( "RASTER_URL": config.veda_raster_api_cf_url, "CLIENT_ID": config.keycloak_ingest_api_client_id, "OPENID_CONFIGURATION_URL": str(config.openid_configuration_url), + "GIT_SHA": config.git_sha, } if config.raster_data_access_role_arn: diff --git a/ingest_api/runtime/src/config.py b/ingest_api/runtime/src/config.py index 11ab3477..9aa4920a 100644 --- a/ingest_api/runtime/src/config.py +++ b/ingest_api/runtime/src/config.py @@ -22,6 +22,7 @@ class Settings(BaseSettings): stac_url: AnyHttpUrl = Field(description="URL of STAC API") root_path: Optional[str] = None stage: Optional[str] = Field(None, description="API stage") + git_sha: Optional[str] = None settings = Settings() diff --git a/ingest_api/runtime/src/monitoring.py b/ingest_api/runtime/src/monitoring.py index 122ed4c6..979844c2 100644 --- a/ingest_api/runtime/src/monitoring.py +++ b/ingest_api/runtime/src/monitoring.py @@ -3,7 +3,7 @@ import time from typing import Callable, Optional -from aws_lambda_powertools import Logger, Metrics, Tracer, single_metric +from aws_lambda_powertools import Logger, Metrics, Tracer from aws_lambda_powertools.metrics import MetricUnit from src.config import ApiSettings @@ -14,6 +14,8 @@ metrics.set_default_dimensions(environment=settings.stage, service="ingest-api") tracer: Tracer = Tracer() +version = settings.git_sha + class ObservabilityMiddleware: """Observability middleware for logging and tracing requests.""" @@ -130,22 +132,20 @@ async def _call_downstream(): logger.append_keys(fastapi=final_ctx) logger.info("Completed request") - with single_metric( - name="http_requests_total", - unit=MetricUnit.Count, - value=1, - default_dimensions=metrics.default_dimensions, - namespace="veda-backend", - ) as m: - m.add_dimension("route_template", route_template) - m.add_dimension("status_family", status_family) - m.add_metric("http_requests_total", 1, MetricUnit.Count) - m.add_metric( - "http_request_duration_ms", elapsed_ms, MetricUnit.Milliseconds - ) - m.add_metric( - "http_response_size_bytes", resp_size_holder["bytes"], MetricUnit.Bytes - ) + metrics.add_dimension("route_template", route_template) + metrics.add_dimension("status_family", status_family) + metrics.add_dimension("git_sha", version) + metrics.add_metric(name="http_requests_total", value=1, unit=MetricUnit.Count) + metrics.add_metric( + name="http_request_duration_ms", + value=elapsed_ms, + unit=MetricUnit.Milliseconds, + ) + metrics.add_metric( + name="http_response_size_bytes", + value=resp_size_holder["bytes"], + unit=MetricUnit.Bytes, + ) def _make_receive_replay(messages): diff --git a/raster_api/infrastructure/config.py b/raster_api/infrastructure/config.py index 5d17cb12..e95c460f 100644 --- a/raster_api/infrastructure/config.py +++ b/raster_api/infrastructure/config.py @@ -2,6 +2,7 @@ `VEDA_RASTER_` will overwrite the values of variables in this file """ +import subprocess from typing import Dict, List, Optional from pydantic import Field @@ -90,6 +91,11 @@ class vedaRasterSettings(BaseSettings): description="Boolean to disable default API gateway endpoints for stac, raster, and ingest APIs. Defaults to false.", ) + git_sha: Optional[str] = Field( + subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode("utf-8"), + description="Git SHA of the current commit, used to track deployment version", + ) + class Config: """model config""" diff --git a/raster_api/runtime/src/config.py b/raster_api/runtime/src/config.py index 49e3bff8..55cda50f 100644 --- a/raster_api/runtime/src/config.py +++ b/raster_api/runtime/src/config.py @@ -58,6 +58,7 @@ class ApiSettings(BaseSettings): debug: bool = False root_path: Optional[str] = None stage: Optional[str] = None + git_sha: Optional[str] = None # MosaicTiler settings enable_mosaic_search: bool = False diff --git a/raster_api/runtime/src/monitoring.py b/raster_api/runtime/src/monitoring.py index 72d57cd1..8b83b663 100644 --- a/raster_api/runtime/src/monitoring.py +++ b/raster_api/runtime/src/monitoring.py @@ -3,7 +3,7 @@ import time from typing import Callable, Optional -from aws_lambda_powertools import Logger, Metrics, Tracer, single_metric +from aws_lambda_powertools import Logger, Metrics, Tracer from aws_lambda_powertools.metrics import MetricUnit from src.config import ApiSettings @@ -14,6 +14,8 @@ metrics.set_default_dimensions(environment=settings.stage, service="raster-api") tracer: Tracer = Tracer() +version = settings.git_sha + class ObservabilityMiddleware: """Observability middleware for logging and tracing requests.""" @@ -130,22 +132,20 @@ async def _call_downstream(): logger.append_keys(fastapi=final_ctx) logger.info("Completed request") - with single_metric( - name="http_requests_total", - unit=MetricUnit.Count, - value=1, - default_dimensions=metrics.default_dimensions, - namespace="veda-backend", - ) as m: - m.add_dimension("route_template", route_template) - m.add_dimension("status_family", status_family) - m.add_metric("http_requests_total", 1, MetricUnit.Count) - m.add_metric( - "http_request_duration_ms", elapsed_ms, MetricUnit.Milliseconds - ) - m.add_metric( - "http_response_size_bytes", resp_size_holder["bytes"], MetricUnit.Bytes - ) + metrics.add_dimension("route_template", route_template) + metrics.add_dimension("status_family", status_family) + metrics.add_dimension("git_sha", version) + metrics.add_metric(name="http_requests_total", value=1, unit=MetricUnit.Count) + metrics.add_metric( + name="http_request_duration_ms", + value=elapsed_ms, + unit=MetricUnit.Milliseconds, + ) + metrics.add_metric( + name="http_response_size_bytes", + value=resp_size_holder["bytes"], + unit=MetricUnit.Bytes, + ) def _make_receive_replay(messages): diff --git a/stac_api/infrastructure/config.py b/stac_api/infrastructure/config.py index 3fde8a7b..9f93884a 100644 --- a/stac_api/infrastructure/config.py +++ b/stac_api/infrastructure/config.py @@ -1,5 +1,6 @@ """Configuration options for the Lambda backed API implementing `stac-fastapi`.""" +import subprocess from typing import Dict, Optional from pydantic import AnyHttpUrl, Field, model_validator @@ -63,6 +64,11 @@ class vedaSTACSettings(BaseSettings): description="Stac version override for Pystac validations https://pystac.readthedocs.io/en/stable/api/version.html", ) + git_sha: Optional[str] = Field( + subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode("utf-8"), + description="Git SHA of the current commit, used to track deployment version", + ) + @model_validator(mode="before") def check_transaction_fields(cls, values): """ diff --git a/stac_api/infrastructure/construct.py b/stac_api/infrastructure/construct.py index efdfe3c7..da84dba3 100644 --- a/stac_api/infrastructure/construct.py +++ b/stac_api/infrastructure/construct.py @@ -55,6 +55,7 @@ def __init__( "DB_MAX_CONN_SIZE": "1", "PYSTAC_STAC_VERSION_OVERRIDE": veda_stac_settings.pystac_stac_version_override, **{k.upper(): v for k, v in veda_stac_settings.env.items()}, + "VEDA_STAC_GIT_SHA": veda_stac_settings.git_sha, } if veda_stac_settings.keycloak_stac_api_client_id is not None: diff --git a/stac_api/runtime/src/config.py b/stac_api/runtime/src/config.py index fa627db7..a61ae375 100644 --- a/stac_api/runtime/src/config.py +++ b/stac_api/runtime/src/config.py @@ -85,6 +85,7 @@ class _ApiSettings(Settings): enable_transactions: bool = Field( False, description="Whether to enable transactions" ) + git_sha: Optional[str] = None @field_validator("cors_origins") @classmethod diff --git a/stac_api/runtime/src/monitoring.py b/stac_api/runtime/src/monitoring.py index eaf30f59..8b540a77 100644 --- a/stac_api/runtime/src/monitoring.py +++ b/stac_api/runtime/src/monitoring.py @@ -3,7 +3,7 @@ import time from typing import Callable, Optional -from aws_lambda_powertools import Logger, Metrics, Tracer, single_metric +from aws_lambda_powertools import Logger, Metrics, Tracer from aws_lambda_powertools.metrics import MetricUnit from src.config import ApiSettings @@ -14,6 +14,8 @@ metrics.set_default_dimensions(environment=settings.stage, service="stac-api") tracer: Tracer = Tracer() +version = settings.git_sha + class ObservabilityMiddleware: """Observability middleware for logging and tracing requests.""" @@ -130,22 +132,20 @@ async def _call_downstream(): logger.append_keys(fastapi=final_ctx) logger.info("Completed request") - with single_metric( - name="http_requests_total", - unit=MetricUnit.Count, - value=1, - default_dimensions=metrics.default_dimensions, - namespace="veda-backend", - ) as m: - m.add_dimension("route_template", route_template) - m.add_dimension("status_family", status_family) - m.add_metric("http_requests_total", 1, MetricUnit.Count) - m.add_metric( - "http_request_duration_ms", elapsed_ms, MetricUnit.Milliseconds - ) - m.add_metric( - "http_response_size_bytes", resp_size_holder["bytes"], MetricUnit.Bytes - ) + metrics.add_dimension("route_template", route_template) + metrics.add_dimension("status_family", status_family) + metrics.add_dimension("git_sha", version) + metrics.add_metric(name="http_requests_total", value=1, unit=MetricUnit.Count) + metrics.add_metric( + name="http_request_duration_ms", + value=elapsed_ms, + unit=MetricUnit.Milliseconds, + ) + metrics.add_metric( + name="http_response_size_bytes", + value=resp_size_holder["bytes"], + unit=MetricUnit.Bytes, + ) def _make_receive_replay(messages): From be46550fd3aa0e4ce6c180a13b4c708003aa6eb7 Mon Sep 17 00:00:00 2001 From: ividito Date: Mon, 15 Sep 2025 10:51:46 -0700 Subject: [PATCH 6/7] fix: stac-api and ingeat-api import errors --- ingest_api/runtime/src/monitoring.py | 4 ++-- stac_api/runtime/src/extension.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ingest_api/runtime/src/monitoring.py b/ingest_api/runtime/src/monitoring.py index 979844c2..e24bd7f4 100644 --- a/ingest_api/runtime/src/monitoring.py +++ b/ingest_api/runtime/src/monitoring.py @@ -5,9 +5,9 @@ from aws_lambda_powertools import Logger, Metrics, Tracer from aws_lambda_powertools.metrics import MetricUnit -from src.config import ApiSettings +from src.config import Settings -settings = ApiSettings() +settings = Settings() logger: Logger = Logger(service="ingest-api", namespace="veda-backend") metrics: Metrics = Metrics(namespace="veda-backend") diff --git a/stac_api/runtime/src/extension.py b/stac_api/runtime/src/extension.py index 62bbe3ba..d62de289 100644 --- a/stac_api/runtime/src/extension.py +++ b/stac_api/runtime/src/extension.py @@ -11,7 +11,7 @@ from stac_fastapi.types.extension import ApiExtension from starlette.requests import Request -from .monitoring import LoggerRouteHandler, tracer +from .monitoring import tracer api_settings = ApiSettings() @@ -30,7 +30,7 @@ def register(self, app: FastAPI, titiler_endpoint: str) -> None: None """ - router = APIRouter(route_class=LoggerRouteHandler) + router = APIRouter() @tracer.capture_method @router.get( From f752d5a0c2b149630870eeaa0d451b90c943021f Mon Sep 17 00:00:00 2001 From: ividito Date: Mon, 15 Sep 2025 14:40:49 -0700 Subject: [PATCH 7/7] fix: increase latency resolution --- docker-compose.yml | 3 +++ ingest_api/runtime/src/config.py | 4 +++- ingest_api/runtime/src/monitoring.py | 8 ++++++-- raster_api/infrastructure/construct.py | 1 + raster_api/runtime/src/monitoring.py | 9 +++++++-- stac_api/runtime/src/monitoring.py | 8 ++++++-- 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8a3a3371..fac1c659 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: # - TITILER_ENDPOINT=raster - TITILER_ENDPOINT=http://0.0.0.0:8082 - VEDA_STAC_ENABLE_TRANSACTIONS=True + - VEDA_STAC_GIT_SHA=local123 depends_on: - database - raster @@ -84,6 +85,7 @@ services: # API Config - VEDA_RASTER_ENABLE_MOSAIC_SEARCH=TRUE - VEDA_RASTER_EXPORT_ASSUME_ROLE_CREDS_AS_ENVS=TRUE + - VEDA_RASTER_GIT_SHA=local123 depends_on: @@ -151,6 +153,7 @@ services: - STAC_URL=http://0.0.0.0:8081 - USERPOOL_ID=us-west-2_123456789 - CLIENT_ID=123456789 + - GIT_SHA=local123 ports: - "8083:8083" command: bash -c "bash /tmp/scripts/wait-for-it.sh -t 120 -h database -p 5432 && python /asset/local.py" diff --git a/ingest_api/runtime/src/config.py b/ingest_api/runtime/src/config.py index 9aa4920a..26b55c18 100644 --- a/ingest_api/runtime/src/config.py +++ b/ingest_api/runtime/src/config.py @@ -22,7 +22,9 @@ class Settings(BaseSettings): stac_url: AnyHttpUrl = Field(description="URL of STAC API") root_path: Optional[str] = None stage: Optional[str] = Field(None, description="API stage") - git_sha: Optional[str] = None + git_sha: Optional[str] = Field( + "", description="Git SHA of the deployed service" + ) # default to str so that docker compose tests work settings = Settings() diff --git a/ingest_api/runtime/src/monitoring.py b/ingest_api/runtime/src/monitoring.py index e24bd7f4..f0c7f5e9 100644 --- a/ingest_api/runtime/src/monitoring.py +++ b/ingest_api/runtime/src/monitoring.py @@ -4,7 +4,7 @@ from typing import Callable, Optional from aws_lambda_powertools import Logger, Metrics, Tracer -from aws_lambda_powertools.metrics import MetricUnit +from aws_lambda_powertools.metrics import MetricResolution, MetricUnit from src.config import Settings settings = Settings() @@ -14,7 +14,10 @@ metrics.set_default_dimensions(environment=settings.stage, service="ingest-api") tracer: Tracer = Tracer() -version = settings.git_sha +try: + version = settings.git_sha[:6] # short git sha +except TypeError: + version = "unknown" class ObservabilityMiddleware: @@ -140,6 +143,7 @@ async def _call_downstream(): name="http_request_duration_ms", value=elapsed_ms, unit=MetricUnit.Milliseconds, + resolution=MetricResolution.High, ) metrics.add_metric( name="http_response_size_bytes", diff --git a/raster_api/infrastructure/construct.py b/raster_api/infrastructure/construct.py index 74e611df..aa6682e2 100644 --- a/raster_api/infrastructure/construct.py +++ b/raster_api/infrastructure/construct.py @@ -59,6 +59,7 @@ def __init__( "VEDA_RASTER_ROOT_PATH": veda_raster_settings.raster_root_path, "VEDA_RASTER_STAGE": stage, "VEDA_RASTER_PROJECT_NAME": veda_raster_settings.project_name, + "VEDA_RASTER_GIT_SHA": veda_raster_settings.git_sha, }, tracing=aws_lambda.Tracing.ACTIVE, ) diff --git a/raster_api/runtime/src/monitoring.py b/raster_api/runtime/src/monitoring.py index 8b83b663..81e278f6 100644 --- a/raster_api/runtime/src/monitoring.py +++ b/raster_api/runtime/src/monitoring.py @@ -4,7 +4,7 @@ from typing import Callable, Optional from aws_lambda_powertools import Logger, Metrics, Tracer -from aws_lambda_powertools.metrics import MetricUnit +from aws_lambda_powertools.metrics import MetricResolution, MetricUnit from src.config import ApiSettings settings = ApiSettings() @@ -14,7 +14,10 @@ metrics.set_default_dimensions(environment=settings.stage, service="raster-api") tracer: Tracer = Tracer() -version = settings.git_sha +try: + version = settings.git_sha[:6] # short git sha +except TypeError: + version = "unknown" class ObservabilityMiddleware: @@ -131,6 +134,7 @@ async def _call_downstream(): } logger.append_keys(fastapi=final_ctx) logger.info("Completed request") + tracer.put_metadata(key="response", value=final_ctx) metrics.add_dimension("route_template", route_template) metrics.add_dimension("status_family", status_family) @@ -140,6 +144,7 @@ async def _call_downstream(): name="http_request_duration_ms", value=elapsed_ms, unit=MetricUnit.Milliseconds, + resolution=MetricResolution.High, ) metrics.add_metric( name="http_response_size_bytes", diff --git a/stac_api/runtime/src/monitoring.py b/stac_api/runtime/src/monitoring.py index 8b540a77..4532320f 100644 --- a/stac_api/runtime/src/monitoring.py +++ b/stac_api/runtime/src/monitoring.py @@ -4,7 +4,7 @@ from typing import Callable, Optional from aws_lambda_powertools import Logger, Metrics, Tracer -from aws_lambda_powertools.metrics import MetricUnit +from aws_lambda_powertools.metrics import MetricResolution, MetricUnit from src.config import ApiSettings settings = ApiSettings() @@ -14,7 +14,10 @@ metrics.set_default_dimensions(environment=settings.stage, service="stac-api") tracer: Tracer = Tracer() -version = settings.git_sha +try: + version = settings.git_sha[:6] # short git sha +except TypeError: + version = "unknown" class ObservabilityMiddleware: @@ -140,6 +143,7 @@ async def _call_downstream(): name="http_request_duration_ms", value=elapsed_ms, unit=MetricUnit.Milliseconds, + resolution=MetricResolution.High, ) metrics.add_metric( name="http_response_size_bytes",