Introduction to Lightstep Tracing
Lightstep is a distributed tracing platform that provides observability for microservices architectures. The Java SDK enables automatic instrumentation and manual tracing for Java applications.
Maven Configuration
<properties> <lightstep.version>0.38.0</lightstep.version> <opentelemetry.version>1.28.0</opentelemetry.version> </properties> <dependencies> <!-- Lightstep SDK --> <dependency> <groupId>com.lightstep.tracer</groupId> <artifactId>lightstep-tracer-jre</artifactId> <version>${lightstep.version}</version> </dependency> <!-- OpenTelemetry API --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-api</artifactId> <version>${opentelemetry.version}</version> </dependency> <!-- OpenTelemetry SDK --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-sdk</artifactId> <version>${opentelemetry.version}</version> </dependency> <!-- OpenTelemetry Exporter --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-otlp</artifactId> <version>${opentelemetry.version}</version> </dependency> <!-- Automatic Instrumentation --> <dependency> <groupId>io.opentelemetry.instrumentation</groupId> <artifactId>opentelemetry-instrumentation-annotations</artifactId> <version>1.28.0</version> </dependency> </dependencies> Core Configuration
Lightstep Tracer Setup
@Configuration @EnableConfigurationProperties(LightstepProperties.class) public class LightstepConfig { private static final Logger logger = LoggerFactory.getLogger(LightstepConfig.class); @Bean @ConditionalOnProperty(name = "lightstep.enabled", havingValue = "true") public Tracer lightstepTracer(LightstepProperties properties) { try { Options options = new Options.OptionsBuilder() .withAccessToken(properties.getAccessToken()) .withComponentName(properties.getComponentName()) .withCollectorHost(properties.getCollectorHost()) .withCollectorPort(properties.getCollectorPort()) .withCollectorProtocol(properties.getCollectorProtocol()) .withVerbosity(properties.getVerbosity()) .withDeadlineMillis(properties.getDeadlineMillis()) .withMaxBufferedSpans(properties.getMaxBufferedSpans()) .withMaxReportingIntervalMillis(properties.getMaxReportingIntervalMillis()) .withDisableReportingLoop(properties.isDisableReportingLoop()) .build(); Tracer tracer = new LightstepTracer(options); tracer.setSamplingRate(properties.getSamplingRate()); logger.info("Lightstep tracer initialized for component: {}", properties.getComponentName()); return tracer; } catch (Exception e) { logger.error("Failed to initialize Lightstep tracer", e); throw new RuntimeException("Lightstep initialization failed", e); } } @Bean @ConditionalOnMissingBean(Tracer.class) public Tracer noopTracer() { return new NoopTracer(); } } @ConfigurationProperties(prefix = "lightstep") public class LightstepProperties { private boolean enabled = true; private String accessToken; private String componentName = "unknown-service"; private String collectorHost = "ingest.lightstep.com"; private int collectorPort = 443; private String collectorProtocol = "https"; private int verbosity = 0; private long deadlineMillis = 30000; private int maxBufferedSpans = 1000; private long maxReportingIntervalMillis = 2500; private boolean disableReportingLoop = false; private float samplingRate = 1.0f; private Map<String, String> tags = new HashMap<>(); // Getters and setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public String getComponentName() { return componentName; } public void setComponentName(String componentName) { this.componentName = componentName; } public String getCollectorHost() { return collectorHost; } public void setCollectorHost(String collectorHost) { this.collectorHost = collectorHost; } public int getCollectorPort() { return collectorPort; } public void setCollectorPort(int collectorPort) { this.collectorPort = collectorPort; } public String getCollectorProtocol() { return collectorProtocol; } public void setCollectorProtocol(String collectorProtocol) { this.collectorProtocol = collectorProtocol; } public int getVerbosity() { return verbosity; } public void setVerbosity(int verbosity) { this.verbosity = verbosity; } public long getDeadlineMillis() { return deadlineMillis; } public void setDeadlineMillis(long deadlineMillis) { this.deadlineMillis = deadlineMillis; } public int getMaxBufferedSpans() { return maxBufferedSpans; } public void setMaxBufferedSpans(int maxBufferedSpans) { this.maxBufferedSpans = maxBufferedSpans; } public long getMaxReportingIntervalMillis() { return maxReportingIntervalMillis; } public void setMaxReportingIntervalMillis(long maxReportingIntervalMillis) { this.maxReportingIntervalMillis = maxReportingIntervalMillis; } public boolean isDisableReportingLoop() { return disableReportingLoop; } public void setDisableReportingLoop(boolean disableReportingLoop) { this.disableReportingLoop = disableReportingLoop; } public float getSamplingRate() { return samplingRate; } public void setSamplingRate(float samplingRate) { this.samplingRate = samplingRate; } public Map<String, String> getTags() { return tags; } public void setTags(Map<String, String> tags) { this.tags = tags; } } OpenTelemetry Integration
OpenTelemetry Configuration
@Component public class OpenTelemetryConfig { @Autowired private LightstepProperties properties; @Bean public OpenTelemetrySdk openTelemetry() { SdkTracerProvider tracerProvider = SdkTracerProvider.builder() .addSpanProcessor(BatchSpanProcessor.builder( OtlpGrpcSpanExporter.builder() .setEndpoint(properties.getCollectorProtocol() + "://" + properties.getCollectorHost() + ":" + properties.getCollectorPort()) .addHeader("lightstep-access-token", properties.getAccessToken()) .build() ).build()) .setSampler(Sampler.traceIdRatioBased(properties.getSamplingRate())) .build(); SdkMeterProvider meterProvider = SdkMeterProvider.builder() .registerMetricReader(PeriodicMetricReader.builder( OtlpGrpcMetricExporter.builder() .setEndpoint(properties.getCollectorProtocol() + "://" + properties.getCollectorHost() + ":" + properties.getCollectorPort()) .addHeader("lightstep-access-token", properties.getAccessToken()) .build() ).build()) .build(); return OpenTelemetrySdk.builder() .setTracerProvider(tracerProvider) .setMeterProvider(meterProvider) .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) .build(); } @Bean public Tracer tracer(OpenTelemetry openTelemetry) { return openTelemetry.getTracer(properties.getComponentName()); } @Bean public Meter meter(OpenTelemetry openTelemetry) { return openTelemetry.getMeter(properties.getComponentName()); } } Manual Tracing
Basic Span Creation
@Service public class OrderService { private final Tracer tracer; private final Meter meter; private final Counter orderCounter; private final Histogram orderDuration; public OrderService(Tracer tracer, Meter meter) { this.tracer = tracer; this.meter = meter; // Initialize metrics this.orderCounter = meter.counterBuilder("orders.total") .setDescription("Total number of orders processed") .build(); this.orderDuration = meter.histogramBuilder("orders.duration") .setDescription("Order processing duration") .setUnit("ms") .build(); } public Order processOrder(OrderRequest request) { // Create a span for the entire operation Span span = tracer.spanBuilder("processOrder") .setAttribute("order.id", request.getOrderId()) .setAttribute("customer.id", request.getCustomerId()) .setAttribute("order.amount", request.getAmount()) .startSpan(); try (Scope scope = span.makeCurrent()) { // Record start time for metrics long startTime = System.currentTimeMillis(); // Business logic with nested spans validateOrder(request); processPayment(request); updateInventory(request); sendConfirmation(request); // Record success span.setStatus(StatusCode.OK); span.setAttribute("order.status", "completed"); // Update metrics orderCounter.add(1); orderDuration.record(System.currentTimeMillis() - startTime); return createOrderResponse(request); } catch (Exception e) { // Record failure span.setStatus(StatusCode.ERROR, e.getMessage()); span.recordException(e); span.setAttribute("order.status", "failed"); throw e; } finally { span.end(); } } private void validateOrder(OrderRequest request) { Span span = tracer.spanBuilder("validateOrder") .setAttribute("order.id", request.getOrderId()) .startSpan(); try (Scope scope = span.makeCurrent()) { // Validation logic if (request.getAmount() <= 0) { throw new IllegalArgumentException("Invalid order amount"); } // Simulate validation work Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); span.recordException(e); throw new RuntimeException("Validation interrupted", e); } catch (Exception e) { span.recordException(e); throw e; } finally { span.end(); } } private void processPayment(OrderRequest request) { Span span = tracer.spanBuilder("processPayment") .setAttribute("order.id", request.getOrderId()) .setAttribute("payment.amount", request.getAmount()) .startSpan(); try (Scope scope = span.makeCurrent()) { // Payment processing logic boolean paymentSuccess = paymentGateway.charge(request); if (!paymentSuccess) { throw new PaymentException("Payment processing failed"); } span.setAttribute("payment.status", "success"); } catch (Exception e) { span.setAttribute("payment.status", "failed"); span.recordException(e); throw e; } finally { span.end(); } } } Annotation-Based Tracing
Custom Tracing Annotations
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Traced { String value() default ""; String[] tags() default {}; boolean recordMetrics() default true; } @Aspect @Component public class TracingAspect { private final Tracer tracer; private final Meter meter; public TracingAspect(Tracer tracer, Meter meter) { this.tracer = tracer; this.meter = meter; } @Around("@annotation(traced)") public Object traceMethod(ProceedingJoinPoint joinPoint, Traced traced) throws Throwable { String spanName = traced.value().isEmpty() ? joinPoint.getSignature().getName() : traced.value(); Span span = tracer.spanBuilder(spanName) .setAttribute("class", joinPoint.getTarget().getClass().getSimpleName()) .setAttribute("method", joinPoint.getSignature().getName()) .startSpan(); // Add custom tags from annotation for (String tag : traced.tags()) { String[] parts = tag.split("="); if (parts.length == 2) { span.setAttribute(parts[0], parts[1]); } } Long startTime = null; if (traced.recordMetrics()) { startTime = System.currentTimeMillis(); } try (Scope scope = span.makeCurrent()) { return joinPoint.proceed(); } catch (Exception e) { span.recordException(e); span.setStatus(StatusCode.ERROR, e.getMessage()); throw e; } finally { if (traced.recordMetrics() && startTime != null) { long duration = System.currentTimeMillis() - startTime; span.setAttribute("duration.ms", duration); // Record metric meter.histogramBuilder(spanName + ".duration") .setUnit("ms") .build() .record(duration); } span.end(); } } } // Usage with annotations @Service public class InventoryService { @Traced(value = "updateInventory", tags = {"operation=update", "domain=inventory"}) public void updateInventory(OrderRequest request) { // Business logic automatically traced inventoryRepository.updateStock(request.getItems()); } @Traced("checkAvailability") public boolean checkAvailability(String productId, int quantity) { return inventoryRepository.getStock(productId) >= quantity; } } HTTP Instrumentation
Web Filter for HTTP Tracing
@Component public class TracingFilter implements Filter { private final Tracer tracer; public TracingFilter(Tracer tracer) { this.tracer = tracer; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { chain.doFilter(request, response); return; } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; // Extract context from headers Context extractedContext = extractContext(httpRequest); Span span = tracer.spanBuilder(httpRequest.getMethod() + " " + httpRequest.getRequestURI()) .setParent(extractedContext) .setSpanKind(SpanKind.SERVER) .setAttribute("http.method", httpRequest.getMethod()) .setAttribute("http.url", httpRequest.getRequestURL().toString()) .setAttribute("http.route", extractRoute(httpRequest)) .setAttribute("http.user_agent", httpRequest.getHeader("User-Agent")) .setAttribute("http.client_ip", getClientIp(httpRequest)) .startSpan(); try (Scope scope = span.makeCurrent()) { chain.doFilter(request, response); // Record response information span.setAttribute("http.status_code", httpResponse.getStatus()); span.setStatus(getSpanStatus(httpResponse.getStatus())); } catch (Exception e) { span.recordException(e); span.setStatus(StatusCode.ERROR, e.getMessage()); throw e; } finally { span.end(); } } private Context extractContext(HttpServletRequest request) { return W3CTraceContextPropagator.getInstance().extract( Context.current(), request, (carrier, key) -> { String value = carrier.getHeader(key); return value != null ? Collections.singletonList(value) : Collections.emptyList(); }); } private String extractRoute(HttpServletRequest request) { String path = request.getRequestURI(); // Convert path parameters to placeholders return path.replaceAll("/\\d+", "/{id}") .replaceAll("/[a-f0-9-]{36}", "/{uuid}"); } private String getClientIp(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } private StatusCode getSpanStatus(int statusCode) { if (statusCode >= 200 && statusCode < 400) { return StatusCode.OK; } else { return StatusCode.ERROR; } } } HTTP Client Instrumentation
@Component public class TracingRestTemplateInterceptor implements ClientHttpRequestInterceptor { private final Tracer tracer; public TracingRestTemplateInterceptor(Tracer tracer) { this.tracer = tracer; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { Span span = tracer.spanBuilder("http.client") .setSpanKind(SpanKind.CLIENT) .setAttribute("http.method", request.getMethod().name()) .setAttribute("http.url", request.getURI().toString()) .startSpan(); // Inject tracing headers W3CTraceContextPropagator.getInstance().inject( Context.current().with(span), request, (carrier, key, value) -> carrier.getHeaders().set(key, value)); try (Scope scope = span.makeCurrent()) { ClientHttpResponse response = execution.execute(request, body); span.setAttribute("http.status_code", response.getRawStatusCode()); span.setStatus(getSpanStatus(response.getRawStatusCode())); return response; } catch (IOException e) { span.recordException(e); span.setStatus(StatusCode.ERROR, e.getMessage()); throw e; } finally { span.end(); } } } Database Tracing
JPA/Hibernate Integration
@Component public class JpaTracingInterceptor implements EmptyInterceptor { private final Tracer tracer; public JpaTracingInterceptor(Tracer tracer) { this.tracer = tracer; } @Override public String onPrepareStatement(String sql) { Span span = tracer.spanBuilder("database.query") .setAttribute("db.system", "postgresql") .setAttribute("db.statement", sql) .setAttribute("db.operation", extractOperation(sql)) .startSpan(); // Store span in current context Context.current().with(span); return sql; } private String extractOperation(String sql) { if (sql == null) return "unknown"; String lowerSql = sql.trim().toLowerCase(); if (lowerSql.startsWith("select")) return "select"; if (lowerSql.startsWith("insert")) return "insert"; if (lowerSql.startsWith("update")) return "update"; if (lowerSql.startsWith("delete")) return "delete"; return "other"; } } @Aspect @Component public class RepositoryTracingAspect { private final Tracer tracer; public RepositoryTracingAspect(Tracer tracer) { this.tracer = tracer; } @Around("execution(* org.springframework.data.repository.Repository+.*(..))") public Object traceRepositoryMethods(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); String repositoryName = joinPoint.getTarget().getClass().getSimpleName(); Span span = tracer.spanBuilder(repositoryName + "." + methodName) .setAttribute("db.operation", methodName) .setAttribute("db.repository", repositoryName) .startSpan(); try (Scope scope = span.makeCurrent()) { Object result = joinPoint.proceed(); // Add result count if applicable if (result instanceof Collection) { span.setAttribute("db.result.count", ((Collection<?>) result).size()); } return result; } catch (Exception e) { span.recordException(e); span.setStatus(StatusCode.ERROR, e.getMessage()); throw e; } finally { span.end(); } } } Custom Metrics and Business Observability
Business Metrics
@Service public class OrderMetricsService { private final Meter meter; private final Counter ordersCreated; private final Counter ordersCompleted; private final Counter ordersFailed; private final Histogram orderValue; private final ObservableGauge inventoryLevel; private final Map<String, Long> inventoryLevels = new ConcurrentHashMap<>(); public OrderMetricsService(Meter meter) { this.meter = meter; // Initialize counters this.ordersCreated = meter.counterBuilder("orders.created") .setDescription("Total orders created") .build(); this.ordersCompleted = meter.counterBuilder("orders.completed") .setDescription("Total orders completed successfully") .build(); this.ordersFailed = meter.counterBuilder("orders.failed") .setDescription("Total orders that failed") .build(); this.orderValue = meter.histogramBuilder("orders.value") .setDescription("Distribution of order values") .setUnit("USD") .build(); // Initialize gauge for inventory levels this.inventoryLevel = meter.gaugeBuilder("inventory.level") .setDescription("Current inventory levels by product") .setUnit("items") .buildWithCallback(measurement -> { inventoryLevels.forEach((productId, level) -> { measurement.record(level, Attributes.of(AttributeKey.stringKey("product.id"), productId)); }); }); } public void recordOrderCreated(Order order) { ordersCreated.add(1); orderValue.record(order.getAmount()); // Record attributes Attributes attributes = Attributes.of( AttributeKey.stringKey("customer.tier"), order.getCustomerTier(), AttributeKey.stringKey("order.type"), order.getType() ); ordersCreated.add(1, attributes); } public void recordOrderCompleted(Order order) { ordersCompleted.add(1); } public void recordOrderFailed(Order order, String reason) { Attributes attributes = Attributes.of( AttributeKey.stringKey("failure.reason"), reason ); ordersFailed.add(1, attributes); } public void updateInventoryLevel(String productId, long level) { inventoryLevels.put(productId, level); } } Distributed Context Propagation
Context Propagation Service
@Service public class ContextPropagationService { public <T> T runWithContext(Callable<T> task, Map<String, String> additionalAttributes) { Span currentSpan = Span.current(); // Add additional attributes to current span if (additionalAttributes != null) { additionalAttributes.forEach(currentSpan::setAttribute); } try { return task.call(); } catch (Exception e) { currentSpan.recordException(e); throw new RuntimeException(e); } } public Runnable wrapWithContext(Runnable task, Map<String, String> attributes) { Context currentContext = Context.current(); return () -> { try (Scope scope = currentContext.makeCurrent()) { if (attributes != null) { Span currentSpan = Span.current(); attributes.forEach(currentSpan::setAttribute); } task.run(); } }; } public Map<String, String> getCurrentContext() { Map<String, String> context = new HashMap<>(); // Extract trace context Span currentSpan = Span.current(); if (currentSpan != null) { context.put("trace_id", currentSpan.getSpanContext().getTraceId()); context.put("span_id", currentSpan.getSpanContext().getSpanId()); } return context; } } Configuration Management
Environment-based Configuration
# application.yml lightstep: enabled: true access-token: ${LIGHTSTEP_ACCESS_TOKEN:} component-name: ${SERVICE_NAME:order-service} collector-host: ${LIGHTSTEP_COLLECTOR_HOST:ingest.lightstep.com} collector-port: ${LIGHTSTEP_COLLECTOR_PORT:443} collector-protocol: https sampling-rate: ${LIGHTSTEP_SAMPLING_RATE:1.0} tags: environment: ${ENVIRONMENT:development} version: ${APP_VERSION:1.0.0} region: ${AWS_REGION:us-east-1} management: endpoints: web: exposure: include: metrics, prometheus metrics: export: lightstep: enabled: true Programmatic Configuration
@Configuration public class LightstepEnvironmentConfig { @Bean @Profile("!test") public Options lightstepOptions() { return new Options.OptionsBuilder() .withAccessToken(getAccessToken()) .withComponentName(getComponentName()) .withCollectorHost(getCollectorHost()) .withCollectorPort(getCollectorPort()) .withCollectorProtocol("https") .withTags(buildTags()) .withMaxBufferedSpans(2000) .withMaxReportingIntervalMillis(5000) .build(); } private String getAccessToken() { return Optional.ofNullable(System.getenv("LIGHTSTEP_ACCESS_TOKEN")) .orElseThrow(() -> new IllegalStateException("LIGHTSTEP_ACCESS_TOKEN is required")); } private String getComponentName() { return Optional.ofNullable(System.getenv("SERVICE_NAME")) .orElse("unknown-service"); } private String getCollectorHost() { return Optional.ofNullable(System.getenv("LIGHTSTEP_COLLECTOR_HOST")) .orElse("ingest.lightstep.com"); } private int getCollectorPort() { return Optional.ofNullable(System.getenv("LIGHTSTEP_COLLECTOR_PORT")) .map(Integer::parseInt) .orElse(443); } private Map<String, String> buildTags() { Map<String, String> tags = new HashMap<>(); tags.put("environment", System.getenv().getOrDefault("ENVIRONMENT", "development")); tags.put("version", System.getenv().getOrDefault("APP_VERSION", "1.0.0")); tags.put("region", System.getenv().getOrDefault("AWS_REGION", "unknown")); tags.put("hostname", getHostname()); return tags; } private String getHostname() { try { return InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { return "unknown"; } } } Testing Support
Test Configuration
@SpringBootTest @TestPropertySource(properties = { "lightstep.enabled=false", "management.metrics.export.lightstep.enabled=false" }) public abstract class BaseIntegrationTest { @MockBean protected Tracer tracer; @MockBean protected Meter meter; @BeforeEach void setupMocks() { SpanContext spanContext = SpanContext.create( "test-trace-id", "test-span-id", TraceFlags.getDefault(), TraceState.getDefault()); Span mockSpan = mock(Span.class); when(mockSpan.getSpanContext()).thenReturn(spanContext); when(tracer.spanBuilder(anyString())).thenReturn(new MockSpanBuilder(mockSpan)); } } @Component @Primary @Profile("test") public class MockTracerConfig { @Bean public Tracer mockTracer() { return new NoopTracer(); } @Bean public Meter mockMeter() { return new NoopMeter(); } } // Custom span builder for testing public class MockSpanBuilder implements SpanBuilder { private final Span span; public MockSpanBuilder(Span span) { this.span = span; } @Override public SpanBuilder setParent(Context context) { return this; } @Override public SpanBuilder setNoParent() { return this; } @Override public SpanBuilder addLink(SpanContext spanContext) { return this; } @Override public SpanBuilder setAttribute(String key, String value) { return this; } @Override public SpanBuilder setAttribute(String key, long value) { return this; } @Override public SpanBuilder setAttribute(String key, double value) { return this; } @Override public SpanBuilder setAttribute(String key, boolean value) { return this; } @Override public SpanBuilder setSpanKind(SpanKind spanKind) { return this; } @Override public SpanBuilder setStartTimestamp(long startTimestamp) { return this; } @Override public Span startSpan() { return span; } } Error Handling and Resilience
Tracing Error Handler
@Component public class TracingErrorHandler { private static final Logger logger = LoggerFactory.getLogger(TracingErrorHandler.class); public void handleTracingError(String operation, Throwable error) { Span currentSpan = Span.current(); if (currentSpan != null) { currentSpan.recordException(error); currentSpan.setAttribute("error.type", error.getClass().getSimpleName()); currentSpan.setAttribute("error.message", error.getMessage()); currentSpan.setStatus(StatusCode.ERROR, operation + " failed"); } logger.error("Tracing operation failed: {}", operation, error); } public <T> Optional<T> executeWithTracing(String operation, Supplier<T> supplier) { Span span = Span.current(); if (span != null) { span.setAttribute("operation", operation); } try { T result = supplier.get(); if (span != null) { span.setAttribute("operation.success", true); } return Optional.of(result); } catch (Exception e) { handleTracingError(operation, e); return Optional.empty(); } } } Conclusion
This Lightstep Java SDK implementation provides:
- Comprehensive Tracing - Automatic and manual instrumentation for applications
- OpenTelemetry Integration - Standards-based tracing with OpenTelemetry
- Spring Boot Support - Auto-configuration and seamless Spring integration
- HTTP Instrumentation - Automatic tracing for web requests and REST clients
- Database Tracing - JPA/Hibernate and repository-level tracing
- Business Metrics - Custom metrics for business observability
- Context Propagation - Distributed context across service boundaries
- Testing Support - Mock implementations for testing environments
The implementation enables full observability for Java applications with minimal code changes while providing powerful insights into application performance and behavior.