developer.overheid.nl

Ontwikkelaarsportaal van de Nederlandse overheid

Ga naar hoofdinhoud

Python voorbeeldimplementatie met Flask

Een voorbeeld hoe Logboek Dataverwerkingen toe te passen met Flask in Python.

Nieuw project​

Maak een nieuw project aan:

uv init --bare ldv-example-python
cd ldv-example-python

Voeg dependency's toe​

# Flask
uv add "flask >=3.1.3, <4.0"

# OpenTelemetry
uv add "opentelemetry-sdk >=1.44.0, <2.0"
uv add "opentelemetry-exporter-otlp >=1.44.0, <2.0"

Logboek​

OpenTelemetry​

In Logboek wordt een OpenTelemetry Tracer geconfigureerd die alleen voor het registreren van verwerkingen gebruikt wordt.

Dit definieert ook een Flask-extentie.

logboek/flask.py
from flask import Config, Flask, current_app
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.semconv.attributes.service_attributes import SERVICE_NAME, SERVICE_VERSION
from opentelemetry.trace import Tracer

from logboek import ProcessingOperator


class Logboek:
def __init__(self, app: Flask | None = None) -> None:
self._processing_operator: ProcessingOperator

if app is not None:
self.init_app(app)

def init_app(self, app: Flask) -> None:
app.extensions["logboek"] = self

config: Config = app.config.setdefault("LOGBOEK", {})

endpoint: str | None = config.setdefault("endpoint")
if not endpoint:
raise RuntimeError("Logboek endpoint must be set")

endpoint_insecure: bool = config.setdefault("endpoint_insecure", False)

tracer = self._create_tracer(
endpoint,
endpoint_insecure,
app.config.setdefault("APP_NAME", ""),
app.config.setdefault("APP_VERSION", ""),
)

self._processing_operator = ProcessingOperator(tracer)

def _create_tracer(self, endpoint: str, endpoint_insecure: bool, app_name: str, app_version: str) -> Tracer:
processor = SimpleSpanProcessor(OTLPSpanExporter(endpoint, endpoint_insecure))

resource: Resource = Resource.create({SERVICE_NAME: app_name, SERVICE_VERSION: app_version})
provider = TracerProvider(resource=resource)
provider.add_span_processor(processor)

return provider.get_tracer("logboek-dataverwerkingen", "0.1.0")

@property
def processing_operator(self) -> ProcessingOperator:
return self._processing_operator


def processing_operator() -> ProcessingOperator:
return current_app.extensions["logboek"].processing_operator

Registreren van verwerkingen​

Met een ProcessingOperator kunnen we eenvoudig verwerkingen starten en direct de juiste attributen toevoegen. Met een Processing kan het "data subject" gezet worden.

logboek/__init__.py
from collections.abc import Generator, Mapping
from contextlib import _GeneratorContextManager, contextmanager

from opentelemetry.context import Context
from opentelemetry.trace import StatusCode, Tracer, use_span
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.trace.span import Span

from logboek.attributes import DATA_SUBJECT_ID, DATA_SUBJECT_ID_TYPE, PROCESSING_ACTIVITY_ID


class Processing:
def __init__(self, span: Span) -> None:
self._span: Span = span

def set_data_subject(self, subject_id: str, sibject_id_type: str) -> None:
self._span.set_attributes(
{
DATA_SUBJECT_ID: subject_id,
DATA_SUBJECT_ID_TYPE: sibject_id_type,
}
)

def successful(self) -> None:
self._span.set_status(StatusCode.OK)

def failed(self, reason: str) -> None:
self._span.set_status(StatusCode.ERROR, reason)


class ProcessingOperator:
def __init__(self, tracer: Tracer) -> None:
self._tracer: Tracer = tracer

def start_proccessing_from_foreign_operation(
self, name: str, activity_id: str, headers: Mapping[str, str], end_on_exit: bool = True
) -> _GeneratorContextManager[Processing]:
context = TraceContextTextMapPropagator().extract(headers)

return self._start_proccessing(name, activity_id, context, end_on_exit)

def start_proccessing(
self, name: str, activity_id: str, end_on_exit: bool = True
) -> _GeneratorContextManager[Processing]:
return self._start_proccessing(name, activity_id, None, end_on_exit)

@contextmanager
def _start_proccessing(
self, name: str, activity_id: str, context: Context | None, end_on_exit: bool
) -> Generator[Processing]:
attributes = {
PROCESSING_ACTIVITY_ID: activity_id,
}

span = self._tracer.start_span(name, attributes=attributes, context=context)

with use_span(span, end_on_exit):
yield Processing(span)

Decorator​

Met een decorator kunnen we eenvoudig verwerkingen registreren.

logboek/decorators.py
import functools
from collections.abc import Callable
from typing import ParamSpec, TypeVar

from flask import request

from .flask import processing_operator

P = ParamSpec("P")
RT = TypeVar("RT")


def logboek_processing(
name: str, activity_id: str, from_foreign: bool = False
) -> Callable[[Callable[P, RT]], Callable[P, RT | None]]:
def decorator(func: Callable[P, RT]) -> Callable[P, RT | None]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> RT | None:
op = processing_operator()

if from_foreign:
cm = op.start_proccessing_from_foreign_operation(
name, activity_id, request.headers, end_on_exit=True
)
else:
cm = op.start_proccessing(name, activity_id, end_on_exit=True)

with cm as processing:
try:
return_value = func(*args, **kwargs)
processing.successful()
return return_value
except Exception as e:
processing.failed(str(e))

return None

return wrapper

return decorator

Applicatie​

Logboek configureren​

Voeg de Logboek-extentie toe en configureer de endpoint-URL.

app/__init__.py
from flask import Flask

from logboek.flask import Logboek

from .persons import persons

app = Flask(__name__)
app.config.from_mapping(
APP_NAME="ldv-example-python",
APP_VERSION="0.1.0",
LOGBOEK={
"endpoint": "127.0.0.1:4317",
"endpoint_insecure": True,
},
)

logboek = Logboek()
logboek.init_app(app)

app.register_blueprint(persons)

Verwerkingen definiëren​

Alles komt nu samen in dit Flask blueprint.

De gemarkeerde regels laten zien hoe de logboek_processing-decorator, en de ProcessingOperator-class en Processing-class gebruikt kunnen worden.

app/persons.py
from time import sleep

from flask import Blueprint

from logboek.decorators import logboek_processing
from logboek.flask import processing_operator

persons = Blueprint("persons", __name__, url_prefix="/persons")


@persons.route("/<bsn>/is-minimal-18-years-old")
@logboek_processing("check_18_or_over", "urn:ldv:activity:1407", from_foreign=True)
def is_minimal_18_years_old(bsn: str) -> dict[str, bool]:
is_18_or_over = _person_is_18_or_over(bsn)

return {"is_18_or_over": is_18_or_over}


def _person_is_18_or_over(bsn: str) -> bool:
with processing_operator().start_proccessing("fetch_date_of_birth", "urn:ldv:activity:1550") as processing:
processing.set_data_subject(bsn, "BSN")

# Simuleer het ophalen van de geboortedatum
sleep(42 / 1000)

processing.successful()

return True

Applicatie starten​

Start nu de applicatie:

.venv/bin/flask run

Roep de API aan:

curl http://127.0.0.1:5000/persons/999990342/is-minimal-18-years-old

Logboek inzien​

Als je de OpenTelemetry viewer gebruikt kun je de twee verwerkingen in het logboek zien.