Manual instrumentation lets you capture business operations that automatic instrumentation misses. Use it to track order processing, payment workflows, or any domain-specific logic that matters for debugging and monitoring.
Prerequisites
- Complete the Java OpenTelemetry instrumentation guide so your app is already sending traces
- Add the OpenTelemetry API dependency to your project
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.57.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>2.23.0</version>
</dependency>
implementation 'io.opentelemetry:opentelemetry-api:1.57.0'
implementation 'io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.23.0'
The versions above are current as of this writing. Check the OpenTelemetry Java releases for the latest versions.
Step 1. Create manual spans
Using annotations (simplest approach)
Annotate methods with @WithSpan to automatically create spans:
import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;
public class OrderService {
@WithSpan("process-order")
public void processOrder(@SpanAttribute("order.id") String orderId) {
// A span wraps this entire method
validateOrder(orderId);
chargePayment(orderId);
shipOrder(orderId);
}
@WithSpan
public void validateOrder(@SpanAttribute("order.id") String orderId) {
// Child span created automatically
}
}
@WithSpancreates a span for the method's lifetime@SpanAttributecaptures method parameters as span attributes- Nested
@WithSpanmethods become child spans automatically
Using the SDK (more control)
For fine-grained control, use the tracer API directly:
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
public class PaymentService {
private static final Tracer tracer = GlobalOpenTelemetry.getTracer("payment-service");
public void processPayment(String paymentId, double amount) {
Span span = tracer.spanBuilder("process-payment").startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("payment.id", paymentId);
span.setAttribute("payment.amount", amount);
// Your payment logic here
chargeCard(paymentId, amount);
} finally {
span.end();
}
}
}
Key points:
GlobalOpenTelemetry.getTracer()gets a tracer from the agent's configured providerspan.makeCurrent()sets this span as the parent for any child spans- Always call
span.end()in a finally block
Creating nested spans
Child spans link to their parent automatically when you use makeCurrent():
public void processOrder(String orderId) {
Span parentSpan = tracer.spanBuilder("process-order").startSpan();
try (Scope parentScope = parentSpan.makeCurrent()) {
parentSpan.setAttribute("order.id", orderId);
// This span becomes a child of process-order
Span validateSpan = tracer.spanBuilder("validate-inventory").startSpan();
try (Scope validateScope = validateSpan.makeCurrent()) {
validateSpan.setAttribute("warehouse", "WH-001");
checkInventory(orderId);
} finally {
validateSpan.end();
}
// Another child span
Span chargeSpan = tracer.spanBuilder("charge-payment").startSpan();
try (Scope chargeScope = chargeSpan.makeCurrent()) {
processPayment(orderId);
} finally {
chargeSpan.end();
}
} finally {
parentSpan.end();
}
}
Step 2. Add attributes and events
Attributes are key-value pairs that show up in SigNoz so you can filter and search traces. Events mark notable moments within a span.
Adding attributes
import io.opentelemetry.api.common.AttributeKey;
Span span = Span.current();
// String attributes
span.setAttribute("user.id", userId);
span.setAttribute("order.status", "processing");
// Numeric attributes
span.setAttribute(AttributeKey.longKey("item.count"), 5L);
span.setAttribute(AttributeKey.doubleKey("total.amount"), 149.99);
// Boolean attributes
span.setAttribute(AttributeKey.booleanKey("premium.customer"), true);
Adding events
Events capture specific moments during span execution:
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributeKey;
Span span = Span.current();
// Simple event
span.addEvent("order.validated");
// Event with attributes
span.addEvent("payment.processed", Attributes.of(
AttributeKey.stringKey("payment.method"), "credit_card",
AttributeKey.stringKey("transaction.id"), "txn_abc123",
AttributeKey.doubleKey("amount"), 99.99
));
// Event with timestamp
span.addEvent("inventory.reserved", Attributes.empty(), Instant.now());
Use events for:
- Cache hits/misses
- Retry attempts
- State transitions
- External API calls
Step 3. Record errors
Flag failures so they surface in SigNoz error views and alerts:
import io.opentelemetry.api.trace.StatusCode;
public void riskyOperation() {
Span span = Span.current();
try {
doSomethingRisky();
span.setStatus(StatusCode.OK);
} catch (Exception e) {
// Record the exception with full stack trace
span.recordException(e);
// Mark the span as errored
span.setStatus(StatusCode.ERROR, e.getMessage());
// Re-throw so calling code can handle it
throw e;
}
}
recordException()captures the stack trace and exception detailssetStatus(StatusCode.ERROR)marks the span red in SigNoz- Always re-throw exceptions unless you're intentionally swallowing them
Getting the current span
From anywhere in your code, grab the active span to add context:
import io.opentelemetry.api.trace.Span;
public void someUtilityMethod(String customerId) {
Span currentSpan = Span.current();
if (currentSpan.isRecording()) {
currentSpan.setAttribute("customer.id", customerId);
currentSpan.addEvent("utility.invoked");
}
}
Step 4. Add span links (optional)
Links connect spans that are causally related but not in a parent-child relationship. Common use cases:
- Batch processing where one trigger creates multiple independent operations
- Fan-out/fan-in patterns
- Async message processing
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.context.Context;
// Save the context from the triggering operation
SpanContext triggerContext = Span.current().getSpanContext();
// Later, create a linked span
Span batchItemSpan = tracer.spanBuilder("process-batch-item")
.addLink(triggerContext)
.startSpan();
try (Scope scope = batchItemSpan.makeCurrent()) {
// Process the batch item
} finally {
batchItemSpan.end();
}
Validate
With your application running, verify traces are being sent to SigNoz:
- Trigger the code paths with your manual instrumentation.
- In SigNoz, open the Services tab and click Refresh. Your application should appear.
- Go to the Traces tab to see your application's traces.
Troubleshooting
Custom spans not appearing?
Check the tracer is initialized:
Tracer tracer = GlobalOpenTelemetry.getTracer("my-service");
// If this returns a no-op tracer, the agent isn't running
Make sure you're running with the Java agent attached.
Verify spans are ended:
// Wrong - span never ends
Span span = tracer.spanBuilder("my-span").startSpan();
doWork();
// Correct - span ends in finally
Span span = tracer.spanBuilder("my-span").startSpan();
try {
doWork();
} finally {
span.end();
}
Child spans not linking to parent?
Use makeCurrent() to set the parent context:
// Wrong - no parent link
Span child = tracer.spanBuilder("child").startSpan();
// Correct - parent span is current
Span parent = tracer.spanBuilder("parent").startSpan();
try (Scope scope = parent.makeCurrent()) {
Span child = tracer.spanBuilder("child").startSpan();
// child is now linked to parent
}
Attributes not showing up?
- Set attributes before calling
span.end() - Use the correct attribute types (strings, longs, doubles, booleans)
- Check attribute names don't have typos
@WithSpan not creating spans?
Make sure:
- The Java agent is attached (annotations only work with the agent)
- The annotated method is called from outside the class (self-invocation skips the proxy)
- You have
opentelemetry-instrumentation-annotationsin your dependencies
Next steps
- Java traces instrumentation guide for auto-instrumentation setup
- Collect Java application logs with OpenTelemetry
- Check OpenTelemetry Java API reference for advanced usage for manual instrumentation