Skip to main content
info

ℹ️ What will you do and learn in this chapter?

  • Know if the flag causes errors or if it is being used
  • Use & create Hooks

Observability & Hooks

What are hooks?

ℹ️ Hooks are a mechanism in OpenFeature that allow you to tap into the feature flag evaluation lifecycle. They let you execute custom code at specific points (stages) during a flag evaluation without modifying the core application logic.

The main stages where you can attach hooks are:

  • Before: Executed before the flag evaluation. Useful for validation or enriching the evaluation context.
  • After: Executed after a successful flag evaluation. Ideal for logging, observability, and telemetry (e.g., sending analytics events).
  • Error: Executed if an error occurs during evaluation. Useful for custom error handling or alerting.
  • Finally: Executed after all other stages, regardless of success or failure. Good for cleanup tasks.

Hooks can be registered at different levels: globally, per-client, or for individual flag invocations.

Track errors in the API using Hooks

📝 Create a new file api/src/main/java/info/touret/musicstore/infrastructure/featureflag/openfeature/ErrorHandlerHook.java with the following content:

package info.touret.musicstore.infrastructure.featureflag.openfeature;

import dev.openfeature.sdk.BooleanHook;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.HookContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.Optional;

public class ErrorHandlerHook implements BooleanHook {
private static final Logger LOGGER = LoggerFactory.getLogger(ErrorHandlerHook.class);

@Override
public Optional<EvaluationContext> before(HookContext<Boolean> ctx, Map<String, Object> hints) {
LOGGER.info(">>> Before evaluating boolean flag: {}", ctx.getFlagKey());
return BooleanHook.super.before(ctx, hints);
}

@Override
public void error(HookContext<Boolean> ctx, Exception error, Map<String, Object> hints) {
LOGGER.error(">>> Unable to process this flag [{}]: {}",ctx.getFlagKey(),error.getMessage());
BooleanHook.super.error(ctx, error, hints);
}
}

📝 In the method getOpenFeatureAPIInstance() of the class OpenFeatureFactory, we will globally register this new hook:

@ApplicationScoped
@Produces
public OpenFeatureAPI getOpenFeatureAPIInstance() {
var openFeatureAPI = OpenFeatureAPI.getInstance();
openFeatureAPI.addHooks(new ErrorHandlerHook());
openFeatureAPI.setProviderAndWait(createProvider());
return openFeatureAPI;
}

📝 To simulate an error, we will also comment the declaration of the targetingKey in the method applyDiscount() of the class DiscountAdapter:

openFeatureAPIClient.setEvaluationContext(new MutableContext()
.add("clientCountry", user.country())
// .add("targetingKey", user.email())
.add("clientEmail", user.email()));

🛠️ If necessary, restart Quarkus:

./mvnw quarkus:dev

🛠️ Run in another terminal this command:

http :8080/instruments User:'{"firstName":"test","lastName":"user1","email":"user11@musician.com","country":"UK"}' accept:"application/json"

👀 You should see these log entries on your console:

2026-05-05 11:15:37,187 INFO [info.touret.musicstore.infrastructure.featureflag.openfeature.ErrorHandlerHook] (quarkus-virtual-thread-0) >>> Before evaluating boolean flag: discount-enabled
2026-05-05 11:15:37,187 ERROR [info.touret.musicstore.infrastructure.featureflag.openfeature.ErrorHandlerHook] (quarkus-virtual-thread-0) >>> Unable to process this flag [discount-enabled]: GO Feature Flag requires a targeting key

📝 Finally, restore the declaration of the targetingKey in the method applyDiscount() of the class DiscountAdapter:

openFeatureAPIClient.setEvaluationContext(new MutableContext()
.add("clientCountry", user.country())
.add("targetingKey", user.email())
.add("clientEmail", user.email()));

Use the OpenTelemetry Hook

Analysing the whole feature-flag's process could be tricky. Let's see how OpenTelemetry can help us.

tip

If you want to know more about observability, feel free to check out this workshop.

Deploy Jaeger

We need first to add Jaeger to collect and get a GUI to navigate through traces.

📝 In the file api/src/main/docker/compose-devservices.yml, add the following lines:

jaeger:
image: jaegertracing/all-in-one:1.76.0
ports:
- "16686:16686"
- "4317:4317"
- "4318:4318"
- "14250:14250"
- "14268:14268"
- "14269:14269"
- "9411:9411"
environment:
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
depends_on:
- go-feature-flag

Configure Go Feature Flag

📝 In the file api/src/main/docker/go-feature-flag/proxy.yaml, add the following configuration lines:

otel:
exporter:
otlp:
endpoint: "http://jaeger:4318"

Add Quarkus's OpenTelemetry support

📝 In the pom.xml file, add the following dependencies:

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-extension-trace-propagators</artifactId>
</dependency>
<dependency>
<groupId>dev.openfeature.contrib.hooks</groupId>
<artifactId>otel</artifactId>
<version>3.3.1</version>
</dependency>

📝 In the api/src/main/resources/application.properties file, add the following configuration:

quarkus.application.name=music-store
quarkus.otel.exporter.otlp.endpoint=http://localhost:4317
quarkus.datasource.jdbc.telemetry=true

Add Traces Hook

📝 In the method getOpenFeatureAPIInstance() of the class OpenFeatureFactory, we will add the new hook and annotate the method with @WithSpan:

@ApplicationScoped
@Produces
@WithSpan
public OpenFeatureAPI getOpenFeatureAPIInstance() {
var openFeatureAPI = OpenFeatureAPI.getInstance();
openFeatureAPI.addHooks(new ErrorHandlerHook(), new TracesHook());
openFeatureAPI.setProviderAndWait(createProvider());
return openFeatureAPI;
}

Add the following import to the class:

import dev.openfeature.contrib.hooks.otel.TracesHook;
import io.opentelemetry.instrumentation.annotations.WithSpan;

Add custom spans

In order to better visualize traces in Jaeger, we can also add some manual spans in our code.

📝 In the class DiscountAdapter, create and inject a Tracer object:

@Inject
Tracer tracer;

📝 In the class DiscountAdapter, update the applyDiscount() method:

@WithSpan
@Override
public Result<Instrument> applyDiscount(Instrument instrument, User user) {
Span discountEnabledspan = tracer.spanBuilder("openfeature-discount-enabled").startSpan();

var openFeatureAPIClient = this.openFeatureAPI.getClient();
openFeatureAPIClient.setEvaluationContext(new MutableContext()
.add("clientCountry", user.country())
.add("targetingKey", user.email())
.add("clientEmail", user.email()));
try (Scope ignored = discountEnabledspan.makeCurrent()) {
var evaluationDetails = openFeatureAPIClient.getBooleanDetails("discount-enabled", false);

LOGGER.info(evaluationDetails.toString());
boolean isDiscountEnabled = evaluationDetails.getValue();
if (isDiscountEnabled) {
double originalPrice = instrument.price();
double discountAmount = openFeatureAPIClient.getDoubleValue("discount-amount", 0.1);
double discountedPrice = originalPrice * (1.0 - discountAmount);
return Result.success(instrument.withDiscount(discountedPrice, originalPrice));
}
return Result.success(instrument);
} finally {
discountEnabledspan.end();
}
}

Add then the following import:

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.context.Scope;
import jakarta.inject.Inject;

🛠️ Restart Quarkus:

./mvnw quarkus:dev

Test

🛠️ Run again K6 to simulate some traffic. In a new terminal, run the following command:

cd infrastructure/scripts
k6 run k6-discount-enabled-test.js

👀 Go to the port screen, check out the 16686 port and click on it to open Jaeger. You should see this screen:

Jaeger Home Screen

🛠️ Make some queries on the traces.

🛠️ Select the go-feature-flag service:

Jaeger Query

🛠️ Click on "Find Traces".

👀 You could see then the corresponding traces.

Jaeger Query

info

ℹ️ You don't see a whole transaction from Quarkus to Go Feature Flag because the configuration is not downloaded through an API call. It's due to the in-process evaluation we discussed earlier. The Java application (via the OpenFeature provider) polls the rules in the background and evaluates the flag locally in its own memory. Since no network call is made at the exact moment of flag evaluation, there is no direct HTTP request to trace between Quarkus and Go Feature Flag during the transaction.