developer.overheid.nl

Ontwikkelaarsportaal van de Nederlandse overheid

Ga naar hoofdinhoud

Java voorbeeldimplementatie met Spring

Een voorbeeld hoe Logboek Dataverwerkingen toe te passen met Spring in Java.

Nieuw project​

Maak een nieuw project aan:

# Stap 1a: Spring Boot CLI
spring init --build maven --name ldv-example-java --group-id nl.example --package-name nl.example.ldv --dependencies web --java-version 21 ldv-example-java

# Stap 1b: Spring Initializr
curl "https://start.spring.io/starter.zip?type=maven-project&language=java&configurationFileFormat=properties&javaVersion=21&name=ldv-example-java&groupId=nl.example&artifactId=ldv-example-java&packageName=nl.example.ldv&dependencies=web" --output ldv-example-java.zip
unzip ldv-example-java.zip -d ldv-example-java

# Stap 2
cd ldv-example-java
tip

Je kunt ook het project genereren via start.spring.io.

Voeg dependency's toe​

pom.xml
<project>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom</artifactId>
<version>1.65.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Ondersteuning voor Aspect-oriented Programming (AOP) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>

<!-- OpenTelemetry -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry.semconv</groupId>
<artifactId>opentelemetry-semconv</artifactId>
<version>[1.43.0, 2.0)</version>
</dependency>
</dependencies>
</project>

Logboek​

OpenTelemetry​

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

src/main/java/nl/example/ldv/logboek/LogboekConfiguration.java
package nl.example.ldv.logboek;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.info.BuildProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.semconv.ServiceAttributes;

@Configuration
public class LogboekConfiguration {

private static final String TRACER_NAME = "logboek-dataverwerkingen";
private static final String TRACER_VERSION = "0.1.0";

private final BuildProperties buildProperties;

public LogboekConfiguration(BuildProperties buildProperties) {
this.buildProperties = buildProperties;
}

@Bean
@Qualifier("logboek")
public Tracer logboekTracer(@Value("${logboek.endpoint}") String endpoint) {
Resource resource = Resource.builder()
.put(ServiceAttributes.SERVICE_NAME, buildProperties.getName())
.put(ServiceAttributes.SERVICE_VERSION, buildProperties.getVersion())
.build();

SpanExporter exporter = OtlpGrpcSpanExporter.builder()
.setEndpoint(endpoint)
.build();

SdkTracerProvider provider = SdkTracerProvider.builder()
.setResource(resource)
.addSpanProcessor(SimpleSpanProcessor.create(exporter))
.setSampler(Sampler.alwaysOn())
.build();

Runtime.getRuntime().addShutdownHook(new Thread(provider::close));

return provider.get(TRACER_NAME, TRACER_VERSION);
}

@Bean
public TextMapPropagator w3cTraceContextPropagator() {
return W3CTraceContextPropagator.getInstance();
}
}
pom.xml
<project>
<!-- ... bestaande configuratie ... -->

<build>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</build>
</project>

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.

src/main/java/nl/example/ldv/logboek/ProcessingOperator.java
package nl.example.ldv.logboek;

import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.ApplicationScope;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.context.propagation.TextMapPropagator;

@ApplicationScope
@Component
public class ProcessingOperator {

private final Tracer tracer;

private final TextMapPropagator propagator;

private static final TextMapGetter<HttpServletRequest> HTTP_REQUEST_GETTER = new HttpRequestGetter();

public ProcessingOperator(@Qualifier("logboek") Tracer tracer, TextMapPropagator propagator) {
this.tracer = tracer;
this.propagator = propagator;
}

public Processing startProccessingFromForeignOperation(String name, String activityId) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
Context remoteContext = propagator.extract(Context.current(), request, HTTP_REQUEST_GETTER);

Span span = buildSpan(name, activityId, remoteContext)
.startSpan();

return new Processing(span);
}

public Processing startProccessing(String name, String activityId) {
Span span = buildSpan(name, activityId, Context.current())
.startSpan();

return new Processing(span);
}

private SpanBuilder buildSpan(String name, String activityId, Context context) {
return tracer.spanBuilder(name)
.setParent(context)
.setAttribute(LogboekAttributes.PROCESSING_ACTIVITY_ID, activityId);
}

private static class HttpRequestGetter implements TextMapGetter<HttpServletRequest> {
@Override
public Iterable<String> keys(HttpServletRequest carrier) {
return Collections.list(carrier.getHeaderNames());
}

@Override
public String get(HttpServletRequest carrier, String key) {
return carrier != null ? carrier.getHeader(key) : null;
}
}
}

Annotatie​

Met een annotatie kunnen we eenvoudig verwerkingen registreren.

src/main/java/nl/example/ldv/logboek/annotation/LogboekProcessing.java
package nl.example.ldv.logboek.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogboekProcessing {

String name();

String activityId();

boolean fromForeign() default false;
}

Applicatie​

Logboek configureren​

Configureer de endpoint-URL.

src/main/resources/application.properties
# ... bestaande configuratie ...

logboek.endpoint=http://127.0.0.1:4317

Verwerkingen definiëren​

Alles komt nu samen in de PersonsController.

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

src/main/java/nl/example/ldv/controller/PersonsController.java
package nl.example.ldv.controller;

import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.opentelemetry.context.Scope;

import nl.example.ldv.logboek.annotation.LogboekProcessing;
import nl.example.ldv.logboek.Processing;
import nl.example.ldv.logboek.ProcessingOperator;

@RestController
@RequestMapping("/persons")
public class PersonsController {

private final ProcessingOperator processingOperator;

public PersonsController(ProcessingOperator processingOperator) {
this.processingOperator = processingOperator;
}

@GetMapping(path = "/{bsn}/is-minimal-18-years-old", produces = MediaType.APPLICATION_JSON_VALUE)
@LogboekProcessing(name = "check_18_or_over", activityId = "urn:ldv:activity:1407", fromForeign = true)
public ResponseEntity<Map<String, Object>> isMinimal18YearsOld(@PathVariable String bsn) throws Throwable {
boolean is18OrOver = personIs18OrOver(bsn);

return ResponseEntity.ok(Map.of("is_18_or_over", is18OrOver));
}

private boolean personIs18OrOver(String bsn) throws Throwable {
Processing processing = processingOperator.startProccessing("fetch_date_of_birth", "urn:ldv:activity:1550");
processing.setDataSubject(bsn, "BSN");

boolean is18OrOver;

try (Scope scope = processing.makeCurrent()) {
// Simuleer het ophalen van de geboortedatum
Thread.sleep(42);
is18OrOver = true;
processing.stopSuccessful();
} catch (Throwable e) {
processing.stopFailed(e.getMessage());
throw e;
}

return is18OrOver;
}
}

Applicatie starten​

Start nu de applicatie:

./mvnw spring-boot:run

Roep de API aan:

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

De verwerkingen zijn nu naar het logboek geschreven.

Logboek inzien​

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