Skip to content

Commit 415726b

Browse files
Merge pull request #226 from stac-utils/feat/telemetry
[WIP] feat: add additional logging and telemetry option in application
2 parents 10a221f + f47fb2f commit 415726b

File tree

6 files changed

+97
-20
lines changed

6 files changed

+97
-20
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release Notes
22

3+
## Unreleased
4+
5+
* update titiler requirement to `>=0.23,<0.24`
6+
37
## 1.8.0 (2025-05-12)
48

59
* update titiler requirement to `>=0.22,<0.23`

pyproject.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ classifiers = [
2828
"Topic :: Scientific/Engineering :: GIS",
2929
]
3030
dependencies = [
31-
"titiler.core>=0.22,<0.23",
32-
"titiler.mosaic>=0.22,<0.23",
31+
"titiler.core>=0.23,<0.24",
32+
"titiler.mosaic>=0.23,<0.24",
3333
"pydantic>=2.4,<3.0",
3434
"pydantic-settings~=2.0",
3535
]
@@ -47,6 +47,13 @@ psycopg-c = [ # C implementation of the libpq wrapper
4747
psycopg-binary = [ # pre-compiled C implementation
4848
"psycopg[binary,pool]"
4949
]
50+
telemetry = [
51+
"opentelemetry-api",
52+
"opentelemetry-sdk",
53+
"opentelemetry-instrumentation-fastapi",
54+
"opentelemetry-instrumentation-logging",
55+
"opentelemetry-exporter-otlp",
56+
]
5057
dev = [
5158
"pre-commit",
5259
"bump-my-version",

titiler/pgstac/backend.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""TiTiler.PgSTAC custom Mosaic Backend and Custom STACReader."""
22

33
import json
4+
import logging
45
from threading import Lock
56
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type
67

@@ -32,6 +33,8 @@
3233
pgstac_config = PgstacSettings()
3334
retry_config = RetrySettings()
3435

36+
logger = logging.getLogger(__name__)
37+
3538

3639
def multi_points_pgstac(
3740
asset_list: Sequence[Dict[str, Any]],
@@ -230,7 +233,10 @@ def get_assets(
230233
else:
231234
raise e
232235

233-
return resp.get("features", [])
236+
features = resp.get("features", [])
237+
238+
logger.info(f"found {len(features)} assets")
239+
return features
234240

235241
@property
236242
def _quadkeys(self) -> List[str]:
@@ -271,6 +277,9 @@ def _reader(
271277
with self.reader(item, tms=self.tms, **self.reader_options) as src_dst:
272278
return src_dst.tile(x, y, z, **kwargs)
273279

280+
logger.info(
281+
f"reading mosaic of {len(mosaic_assets)} assets with reader: {self.reader}"
282+
)
274283
img, used_assets = mosaic_reader(
275284
mosaic_assets, _reader, tile_x, tile_y, tile_z, **kwargs
276285
)
@@ -315,6 +324,7 @@ def _reader(
315324
if "allowed_exceptions" not in kwargs:
316325
kwargs.update({"allowed_exceptions": (PointOutsideBounds,)})
317326

327+
logger.info(f"reading asset values for points with reader: {self.reader}")
318328
return list(
319329
multi_points_pgstac(mosaic_assets, _reader, lon, lat, **kwargs).items()
320330
)
@@ -354,6 +364,9 @@ def _reader(item: Dict[str, Any], bbox: BBox, **kwargs: Any) -> ImageData:
354364
with self.reader(item, **self.reader_options) as src_dst:
355365
return src_dst.part(bbox, **kwargs)
356366

367+
logger.info(
368+
f"reading mosaic of {len(mosaic_assets)} assets with reader: {self.reader}"
369+
)
357370
img, used_assets = mosaic_reader(
358371
mosaic_assets,
359372
_reader,
@@ -402,6 +415,9 @@ def _reader(item: Dict[str, Any], shape: Dict, **kwargs: Any) -> ImageData:
402415
with self.reader(item, **self.reader_options) as src_dst:
403416
return src_dst.feature(shape, **kwargs)
404417

418+
logger.info(
419+
f"reading mosaic of {len(mosaic_assets)} assets with reader: {self.reader}"
420+
)
405421
img, used_assets = mosaic_reader(
406422
mosaic_assets,
407423
_reader,

titiler/pgstac/factory.py

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Custom MosaicTiler Factory for PgSTAC Mosaic Backend."""
22

3+
import logging
34
import os
45
import re
56
import warnings
@@ -67,6 +68,8 @@
6768
"yes",
6869
]
6970

71+
logger = logging.getLogger(__name__)
72+
7073

7174
def _first_value(values: List[Any], default: Any = None):
7275
"""Return the first not None value."""
@@ -117,14 +120,14 @@ class MosaicTilerFactory(BaseFactory):
117120
conforms_to: Set[str] = field(
118121
factory=lambda: {
119122
# https://docs.ogc.org/is/20-057/20-057.html#toc30
120-
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/tileset",
123+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tileset",
121124
# https://docs.ogc.org/is/20-057/20-057.html#toc34
122-
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/tilesets-list",
125+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tilesets-list",
123126
# https://docs.ogc.org/is/20-057/20-057.html#toc65
124-
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/core",
125-
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/png",
126-
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/jpeg",
127-
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/tiff",
127+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/core",
128+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/png",
129+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/jpeg",
130+
"http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tiff",
128131
}
129132
)
130133

@@ -196,6 +199,7 @@ def tilejson(
196199
render_params=Depends(self.render_dependency),
197200
):
198201
"""Return TileJSON document for a search_id."""
202+
logger.info(f"fetching search info for search {search_id}")
199203
with request.app.state.dbpool.connection() as conn:
200204
with conn.cursor(row_factory=class_row(model.Search)) as cursor:
201205
cursor.execute(
@@ -292,6 +296,7 @@ def wmts( # noqa: C901
292296
search_id=Depends(self.path_dependency),
293297
):
294298
"""OGC WMTS endpoint."""
299+
logger.info(f"fetching search info for search {search_id}")
295300
with request.app.state.dbpool.connection() as conn:
296301
with conn.cursor(row_factory=class_row(model.Search)) as cursor:
297302
cursor.execute(
@@ -379,6 +384,16 @@ def wmts( # noqa: C901
379384
stacklevel=2,
380385
)
381386

387+
bbox_crs_type = "WGS84BoundingBox"
388+
bbox_crs_uri = "urn:ogc:def:crs:OGC:2:84"
389+
if tms.rasterio_geographic_crs != WGS84_CRS:
390+
bbox_crs_type = "BoundingBox"
391+
bbox_crs_uri = CRS_to_urn(tms.rasterio_geographic_crs)
392+
# WGS88BoundingBox is always xy ordered, but BoundingBox must match the CRS order
393+
if crs_axis_inverted(tms.geographic_crs):
394+
# match the bounding box coordinate order to the CRS
395+
bounds = [bounds[1], bounds[0], bounds[3], bounds[2]]
396+
382397
# LAYER from query-parameters
383398
qs_key_to_remove = [
384399
"tilematrixsetid",
@@ -419,16 +434,6 @@ def wmts( # noqa: C901
419434
"Could not find any valid layers in metadata or construct one from Query Parameters."
420435
)
421436

422-
bbox_crs_type = "WGS84BoundingBox"
423-
bbox_crs_uri = "urn:ogc:def:crs:OGC:2:84"
424-
if tms.rasterio_geographic_crs != WGS84_CRS:
425-
bbox_crs_type = "BoundingBox"
426-
bbox_crs_uri = CRS_to_urn(tms.rasterio_geographic_crs)
427-
# WGS88BoundingBox is always xy ordered, but BoundingBox must match the CRS order
428-
if crs_axis_inverted(tms.geographic_crs):
429-
# match the bounding box coordinate order to the CRS
430-
bounds = [bounds[1], bounds[0], bounds[3], bounds[2]]
431-
432437
return self.templates.TemplateResponse(
433438
request,
434439
name="wmts.xml",
@@ -486,15 +491,19 @@ def geojson_statistics(
486491
fc = FeatureCollection(type="FeatureCollection", features=[geojson])
487492

488493
with rasterio.Env(**env):
494+
logger.info(
495+
f"opening data with backend: {self.backend} and reader {self.dataset_reader}"
496+
)
489497
with self.backend(
490498
search_id,
491499
reader=self.dataset_reader,
492500
reader_options=reader_params.as_dict(),
493501
**backend_params.as_dict(),
494502
) as src_dst:
495-
for feature in fc.features:
503+
for i, feature in enumerate(fc.features):
496504
shape = feature.model_dump(exclude_none=True)
497505

506+
logger.info(f"{i}: reading data")
498507
image, _ = src_dst.feature(
499508
shape,
500509
shape_crs=coord_crs or WGS84_CRS,
@@ -514,8 +523,10 @@ def geojson_statistics(
514523
)
515524

516525
if post_process:
526+
logger.info(f"{i}: post processing image")
517527
image = post_process(image)
518528

529+
logger.info(f"{i}: calculating statistics")
519530
stats = image.statistics(
520531
**stats_params.as_dict(),
521532
hist_options=histogram_params.as_dict(),
@@ -569,6 +580,9 @@ def bbox_image(
569580
):
570581
"""Create image from a bbox."""
571582
with rasterio.Env(**env):
583+
logger.info(
584+
f"opening data with backend: {self.backend} and reader {self.dataset_reader}"
585+
)
572586
with self.backend(
573587
search_id,
574588
reader=self.dataset_reader,
@@ -588,6 +602,7 @@ def bbox_image(
588602
dst_colormap = getattr(src_dst, "colormap", None)
589603

590604
if post_process:
605+
logger.info("post processing image")
591606
image = post_process(image)
592607

593608
content, media_type = self.render_func(
@@ -643,6 +658,9 @@ def feature_image(
643658
):
644659
"""Create image from a geojson feature."""
645660
with rasterio.Env(**env):
661+
logger.info(
662+
f"opening data with backend: {self.backend} and reader {self.dataset_reader}"
663+
)
646664
with self.backend(
647665
search_id,
648666
reader=self.dataset_reader,
@@ -662,6 +680,7 @@ def feature_image(
662680
)
663681

664682
if post_process:
683+
logger.info("post processing image")
665684
image = post_process(image)
666685

667686
content, media_type = self.render_func(

titiler/pgstac/main.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ async def lifespan(app: FastAPI):
123123

124124
optional_headers = []
125125
if settings.debug:
126+
logging.getLogger("titiler").setLevel(logging.INFO)
127+
126128
app.add_middleware(TotalTimeMiddleware)
127129
app.add_middleware(LoggerMiddleware)
128130

@@ -167,6 +169,33 @@ def pgstac_info(request: Request) -> Dict:
167169
}
168170

169171

172+
if settings.telemetry_enabled:
173+
from opentelemetry import trace
174+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
175+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
176+
from opentelemetry.instrumentation.logging import LoggingInstrumentor
177+
from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION, Resource
178+
from opentelemetry.sdk.trace import TracerProvider
179+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
180+
181+
LoggingInstrumentor().instrument(set_logging_format=True)
182+
FastAPIInstrumentor.instrument_app(app)
183+
184+
resource = Resource.create(
185+
{
186+
SERVICE_NAME: "titiler.pgstac",
187+
SERVICE_VERSION: titiler_pgstac_version,
188+
}
189+
)
190+
191+
provider = TracerProvider(resource=resource)
192+
193+
# uses the OTEL_EXPORTER_OTLP_ENDPOINT env var
194+
processor = BatchSpanProcessor(OTLPSpanExporter())
195+
provider.add_span_processor(processor)
196+
197+
trace.set_tracer_provider(provider)
198+
170199
TITILER_CONFORMS_TO = {
171200
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/core",
172201
"http://www.opengis.net/spec/ogcapi-common-1/1.0/req/landing-page",

titiler/pgstac/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class ApiSettings(BaseSettings):
3232
enable_assets_endpoints: bool = False
3333
enable_external_dataset_endpoints: bool = False
3434

35+
telemetry_enabled: bool = False
36+
3537
model_config = SettingsConfigDict(
3638
env_prefix="TITILER_PGSTAC_API_", env_file=".env", extra="ignore"
3739
)

0 commit comments

Comments
 (0)