Skip to content
Open
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
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ VENV := .venv
BIN := $(VENV)/bin
UV := uv
PIP := $(BIN)/pip
LOG_FILE := runtime-logs.log
IRI_LOG_FILE ?= $(LOG_FILE)

STAMP_VENV := $(VENV)/.created
STAMP_DEPS := $(VENV)/.deps
Expand Down Expand Up @@ -34,6 +36,7 @@ dev: deps
IRI_API_ADAPTER_compute=app.demo_adapter.DemoAdapter \
IRI_API_ADAPTER_filesystem=app.demo_adapter.DemoAdapter \
IRI_API_ADAPTER_task=app.demo_adapter.DemoAdapter \
IRI_LOG_FILE="$${IRI_LOG_FILE:-$${LOG_FILE:-$(IRI_LOG_FILE)}}" \
DEMO_QUEUE_UPDATE_SECS=2 \
OPENTELEMETRY_ENABLED=true \
API_URL_ROOT='http://localhost:8000' fastapi dev
Expand Down Expand Up @@ -78,4 +81,3 @@ ARGS ?=
# call it via: make manage-globus ARGS=scopes-show
manage-globus: deps
@source local.env && $(BIN)/python ./tools/manage_globus.py $(ARGS)

6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ Links to data, created by this api, will concatenate these values producing link
- `IRI_API_PARAMS`: as described above, this is a way to customize the API meta-data
- `IRI_API_ADAPTER_*`: these values specify the business logic for the per-api-group implementation of a facility_adapter. For example: `IRI_API_ADAPTER_status=myfacility.MyFacilityStatusAdapter` would load the implementation of the `app.routers.status.facility_adapter.FacilityAdapter` abstract class to handle the `status` business logic for your facility.
- `IRI_SHOW_MISSING_ROUTES`: hide api groups that don't have an `IRI_API_ADAPTER_*` environment variable defined, if set to `true`. This way if your facility only wishes to expose some api groups but not others, they can be hidden. (Defaults to `false`.)
- `LOG_LEVEL`: logging level for the API and adapters. Defaults to `DEBUG`.
- `IRI_LOG_FILE`: file path for API logs. Logs always go to stdout; when this is set, logs also go to the file.
- `LOG_FILE`: fallback file path for API logs when `IRI_LOG_FILE` is not set.

For local development, `make` writes logs to `runtime-logs.log` by default. Use `make LOG_FILE=/tmp/iri-api.log` or `make IRI_LOG_FILE=/tmp/iri-api.log` to choose a different file. You can also put either variable in `local.env`.

## Docker support

Expand Down Expand Up @@ -142,4 +147,3 @@ You can optionally use globus for authorization. Steps to use globus:
- Specify the monitoring endpoint by setting the [OpenTelemetry](https://opentelemetry.io/docs/zero-code/python/) env vars
- Add additional routers for other API-s
- Add authenticated API-s via an [OAuth2 integration](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/)

85 changes: 74 additions & 11 deletions app/apilogger.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,91 @@
"""Logging utilities for the IRI Facility API."""

import logging
import os
import sys
from pathlib import Path

LEVELS = {"FATAL": logging.FATAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG}

DEFAULT_FORMAT = "%(asctime)s.%(msecs)03d - %(name)s - %(levelname)s - %(message)s"
DEFAULT_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S"
IRI_HANDLER_ATTR = "_iri_facility_api_handler"

def get_stream_logger(name: str = __name__, level: str = "DEBUG") -> logging.Logger:
_CONFIGURED = False


def _level(level: str | int | None) -> int:
if isinstance(level, int):
return level
return LEVELS.get(str(level or "INFO").upper(), logging.INFO)


def _log_file_path() -> Path | None:
log_file = os.environ.get("IRI_LOG_FILE") or os.environ.get("LOG_FILE")
return Path(log_file) if log_file else None


def configure_logging(level: str | int | None = None) -> None:
"""
Return a configured Stream logger.
Configure root logging for the API.

Logs always go to stdout. If IRI_LOG_FILE or LOG_FILE is set, logs also go
to that file.
"""
logger = logging.getLogger(name)
global _CONFIGURED

log_level = _level(level or os.environ.get("LOG_LEVEL"))
root = logging.getLogger()
root.setLevel(log_level)

if not logger.handlers:
handler = logging.StreamHandler()
if _CONFIGURED:
for handler in root.handlers:
if getattr(handler, IRI_HANDLER_ATTR, False):
handler.setLevel(log_level)
return

formatter = logging.Formatter("%(asctime)s.%(msecs)03d - %(name)s - %(levelname)s - %(message)s", datefmt="%a, %d %b %Y %H:%M:%S")
formatter = logging.Formatter(DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT)

handler.setFormatter(formatter)
logger.addHandler(handler)
for handler in root.handlers[:]:
if getattr(handler, IRI_HANDLER_ATTR, False):
root.removeHandler(handler)
handler.close()

stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(log_level)
stdout_handler.setFormatter(formatter)
setattr(stdout_handler, IRI_HANDLER_ATTR, True)
root.addHandler(stdout_handler)

log_file = _log_file_path()
if log_file:
if log_file.parent != Path("."):
log_file.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(log_file)
file_handler.setLevel(log_level)
file_handler.setFormatter(formatter)
setattr(file_handler, IRI_HANDLER_ATTR, True)
root.addHandler(file_handler)

_CONFIGURED = True


def get_stream_logger(name: str = __name__, level: str = "DEBUG") -> logging.Logger:
"""
Return a logger using the shared API stdout and optional file logging setup.
"""
configure_logging(level)

logger = logging.getLogger(name)
logger.setLevel(_level(level))
logger.propagate = True

logger.setLevel(LEVELS.get(level, logging.DEBUG))
logger.propagate = False
for handler in logger.handlers[:]:
logger.removeHandler(handler)
handler.close()

return logger
return logger
10 changes: 4 additions & 6 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

from . import config
from .apilogger import configure_logging

from app.routers.error_handlers import install_error_handlers
from app.routers.facility import facility
from app.routers.status import status
Expand All @@ -19,12 +22,7 @@
from app.routers.filesystem import filesystem
from app.routers.task import task

from . import config

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s"
)
configure_logging(config.LOG_LEVEL)

# ------------------------------------------------------------------
# OpenTelemetry Tracing Configuration
Expand Down
Loading