When your system spans multiple services, understanding what's happening under the hood becomes crucial for debugging, performance optimization, and maintaining reliability. This is where observability comes in.
OpenObserve, an open-source observability platform, offers a powerful, cost-effective solution for collecting and visualizing logs, traces, and metrics. In this guide, I'll show you how to integrate OpenObserve with a Spring Boot application, including a multi-service architecture example that demonstrates real-world monitoring scenarios.
What you'll learn:
- The importance of observability in modern applications
- How to set up OpenObserve with Docker
- Integrating OpenTelemetry for distributed tracing
- Sending structured logs directly to OpenObserve
- Analyzing traces and logs together for faster debugging
- Error simulation and monitoring in action
Video Tutorial: Watch the complete walkthrough on YouTube
Why Observability Matters
Before diving into the technical implementation, let's understand why observability is critical:
1. Troubleshooting Distributed Systems
In a microservices architecture, a single user request might traverse multiple services. Without proper tracing, finding the root cause of an error becomes like finding a needle in a haystack.
2. Performance Optimization
Understanding which services are slow and why helps you optimize critical paths.
3. Proactive Issue Detection
Monitoring logs and traces allows you to detect and resolve issues before they impact users.
4. Compliance and Auditing
Having a complete record of what happened in your system helps with compliance requirements.
5. Business Intelligence
Observability data can reveal patterns in user behavior and system usage.
What We're Building
We'll build a full-stack e-commerce monitoring demonstration featuring:
- A React frontend (Amazon-style store)
- A Spring Boot 3 backend with distributed microservices
- OpenObserve for centralized logging and tracing
Architecture Overview
┌────────────────────────────────────────────────────────┐
│ React Frontend (Port 3000) │
└───────────────────────────┬────────────────────────────┘
│ HTTP POST /api/orders/checkout
▼
┌────────────────────────────────────────────────────────┐
│ order-service Gateway (Port 8080) │
└───────┬───────────────────┬────────────────────┬───────┘
│ HTTP POST │ HTTP POST │ HTTP POST
▼ ▼ ▼
┌──────────────┐ ┌────────────────┐ ┌─────────────────────┐
│ inventory- │ │ payment-service│ │ fulfillment-service │
│ service │ │ (Port 8082) │ │ (Port 8083) │
│ (Port 8081) │ └────────────────┘ └─────────────────────┘
└──────────────┘
The stack consists of 4 independent Spring Boot microservices:
-
order-service(localhost:8080): Primary API Gateway & Order Checkout Coordinator -
inventory-service(localhost:8081): Stock management -
payment-service(localhost:8082): Payment processing -
fulfillment-service(localhost:8083): Shipping logistics
All services send logs and traces to OpenObserve, enabling end-to-end visibility.
Prerequisites
Before we begin, ensure you have:
- Docker Desktop installed and running
- Node.js (v18+) installed
- Java 17 JDK installed
- Maven (or use the auto-download in the provided script)
- PowerShell (Windows) or terminal for your OS
Step 1: Setting Up OpenObserve
1.1. Docker Compose Configuration
Create a docker-compose.yml file:
version: '3.8'
services:
openobserve:
image: openobserve/openobserve:latest
container_name: openobserve
restart: always
environment:
- ZO_ROOT_USER_EMAIL=root@example.com
- ZO_ROOT_USER_PASSWORD=ComplexPassword123
- ZO_HTTP_PORT=5080
- ZO_DATA_DIR=/data
ports:
- "5080:5080"
volumes:
- openobserve-data:/data
volumes:
openobserve-data:
This configuration:
- Pulls the official OpenObserve image
- Sets up default credentials (
root@example.com/ComplexPassword123) - Exposes the UI on port 5080
- Persists data using a Docker volume
1.2. Start OpenObserve
Run the following command:
docker-compose up -d
Wait a few seconds for the container to initialize, then navigate to http://localhost:5080 and log in with:
-
Email:
root@example.com -
Password:
ComplexPassword123

OpenObserve login page with default credentials
Step 2: Spring Boot Application Configuration
2.1. Maven Dependencies
Add the following dependencies to your pom.xml:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom</artifactId>
<version>1.35.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer & OpenTelemetry Tracing -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2.2. Application Configuration
Add OpenObserve configuration to application.yml:
server:
port: 8080
spring:
application:
name: order-service
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,env
tracing:
sampling:
probability: 1.0
otlp:
tracing:
endpoint: http://localhost:5080/api/default/v1/traces
headers:
Authorization: "Basic cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4UGFzc3dvcmQxMjM="
opentelemetry:
resource-attributes:
service.name: order-service
openobserve:
url: http://localhost:5080
auth-header: "Basic cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4UGFzc3dvcmQxMjM="
organization: default
stream: default
Note: The
Authorizationheader uses the Base64-encoded credentials. The format isBasic base64(email:password).
Step 3: Implementing Log Forwarding
3.1. The Log Publisher
Create a component that queues and flushes logs to OpenObserve:
package com.example.amzstore.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
@Slf4j
@Component
@EnableScheduling
@RequiredArgsConstructor
public class OpenObserveLogPublisher {
@Value("${openobserve.url:http://localhost:5080}")
private String openobserveUrl;
@Value("${openobserve.auth-header:Basic cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4UGFzc3dvcmQxMjM=}")
private String authHeader;
private final RestTemplate restTemplate;
private static final ConcurrentLinkedQueue<Map<String, Object>> logQueue = new ConcurrentLinkedQueue<>();
public static void queueLog(String level, String loggerName, String message,
String traceId, String spanId) {
if (loggerName != null && loggerName.contains("OpenObserveLogPublisher")) {
return;
}
Map<String, Object> logEntry = new HashMap<>();
logEntry.put("_timestamp", Instant.now().toEpochMilli() * 1000);
logEntry.put("level", level);
logEntry.put("logger", loggerName != null ? loggerName : "ApplicationLogger");
logEntry.put("message", message);
logEntry.put("service_name", "amzstore-backend");
logEntry.put("trace_id", traceId != null ? traceId : "none");
logEntry.put("span_id", spanId != null ? spanId : "none");
logQueue.add(logEntry);
}
@Scheduled(fixedRate = 1000)
public void flushLogsToOpenObserve() {
if (logQueue.isEmpty()) {
return;
}
List<Map<String, Object>> batch = new ArrayList<>();
while (!logQueue.isEmpty() && batch.size() < 100) {
Map<String, Object> entry = logQueue.poll();
if (entry != null) {
batch.add(entry);
}
}
if (batch.isEmpty()) return;
try {
String endpoint = openobserveUrl + "/api/default/default/_json";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", authHeader);
HttpEntity<List<Map<String, Object>>> entity = new HttpEntity<>(batch, headers);
restTemplate.postForEntity(endpoint, entity, String.class);
} catch (Exception e) {
log.debug("Failed to send logs to OpenObserve: {}", e.getMessage());
}
}
}
3.2. Custom Logback Appender
To automatically capture all log events, create a Logback appender:
package com.example.amzstore.config;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
public class OpenObserveLogbackAppender extends AppenderBase<ILoggingEvent> {
@Override
protected void append(ILoggingEvent eventObject) {
if (eventObject == null) return;
String traceId = "none";
String spanId = "none";
if (eventObject.getMDCPropertyMap() != null) {
traceId = eventObject.getMDCPropertyMap().getOrDefault("traceId", "none");
spanId = eventObject.getMDCPropertyMap().getOrDefault("spanId", "none");
}
OpenObserveLogPublisher.queueLog(
eventObject.getLevel().toString(),
eventObject.getLoggerName(),
eventObject.getFormattedMessage(),
traceId,
spanId
);
}
}
3.3. Logback Configuration
Update logback-spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Console Appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [traceId=%X{traceId:-none} spanId=%X{spanId:-none}] - %msg%n</pattern>
</encoder>
</appender>
<!-- OpenObserve Appender -->
<appender name="OPENOBSERVE" class="com.example.amzstore.config.OpenObserveLogbackAppender">
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="OPENOBSERVE"/>
</root>
</configuration>
Step 4: Distributed Tracing Implementation
4.1. HTTP Logging Interceptor
Create an interceptor to capture all HTTP requests and responses with trace IDs:
package com.example.amzstore.config;
import io.micrometer.tracing.Tracer;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
@Component
@RequiredArgsConstructor
public class LogInterceptor implements HandlerInterceptor {
private final Tracer tracer;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String traceId = tracer.currentSpan() != null ?
tracer.currentSpan().context().traceId() : "none";
String spanId = tracer.currentSpan() != null ?
tracer.currentSpan().context().spanId() : "none";
String msg = String.format("HTTP %s %s from %s",
request.getMethod(), request.getRequestURI(), request.getRemoteAddr());
OpenObserveLogPublisher.queueLog("INFO", "HTTP_REQUEST", msg, traceId, spanId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
String traceId = tracer.currentSpan() != null ?
tracer.currentSpan().context().traceId() : "none";
String spanId = tracer.currentSpan() != null ?
tracer.currentSpan().context().spanId() : "none";
String level = response.getStatus() >= 400 ? "ERROR" : "INFO";
String msg = String.format("HTTP %s %s completed with status %d",
request.getMethod(), request.getRequestURI(), response.getStatus());
if (ex != null) {
msg += " Exception: " + ex.getMessage();
}
OpenObserveLogPublisher.queueLog(level, "HTTP_RESPONSE", msg, traceId, spanId);
}
}
Register the interceptor:
package com.example.amzstore.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor);
}
}
4.2. Manual Tracing in Services
To create custom spans in your services:
import io.micrometer.tracing.ScopedSpan;
import io.micrometer.tracing.Tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
@Service
@RequiredArgsConstructor
public class OrderService {
private final Tracer tracer;
public Order processCheckout(CheckoutRequest request) {
ScopedSpan span = tracer.startScopedSpan("AuthService :: validateCustomerSession");
try {
span.tag("peer.service", "auth-service");
span.tag("order.id", orderId);
span.tag("customer.email", email);
// Your business logic here
Span.current().setStatus(StatusCode.OK,
"AuthService: Customer session token validated successfully");
} catch (Exception e) {
span.error(e);
Span.current().recordException(e);
Span.current().setStatus(StatusCode.ERROR, "Authentication failed: " + e.getMessage());
throw e;
} finally {
span.end();
}
}
}
4.3. Cross-Service Trace Propagation
For HTTP calls between services, the trace context is automatically propagated using W3C Trace Context headers when using RestTemplate:
@Autowired
private RestTemplate restTemplate;
public boolean callInventoryService() {
// The trace context is automatically propagated via headers
// No additional code needed - OpenTelemetry handles it!
restTemplate.postForEntity(inventoryServiceUrl + "/reserve", null, Map.class);
}
Step 5: Running the Demo Application
5.1. Automated Launch Script
The repository includes a PowerShell script that automates the entire setup:
# run-all.ps1
Write-Host "Starting OpenObserve Multi-Microservice E-Commerce Monitoring Stack" -ForegroundColor Cyan
# Kill any existing Java processes
Stop-Process -Name java -Force -ErrorAction SilentlyContinue
# Start OpenObserve
docker-compose up -d
# Check for Maven and download if needed
# ... (Maven setup logic)
# Start all microservices
Start-Process powershell.exe -ArgumentList "-NoExit", "-Command", "& mvn spring-boot:run" -WorkingDirectory "./services/inventory-service"
Start-Process powershell.exe -ArgumentList "-NoExit", "-Command", "& mvn spring-boot:run" -WorkingDirectory "./services/payment-service"
Start-Process powershell.exe -ArgumentList "-NoExit", "-Command", "& mvn spring-boot:run" -WorkingDirectory "./services/fulfillment-service"
Start-Process powershell.exe -ArgumentList "-NoExit", "-Command", "& mvn spring-boot:run" -WorkingDirectory "./backend"
# Start React frontend
npm install --prefix ./frontend
Start-Process powershell.exe -ArgumentList "-NoExit", "-Command", "npm run dev" -WorkingDirectory "./frontend"
5.2. Manual Start
If you prefer to start services manually:
1. Start OpenObserve:
docker-compose up -d
2. Start each microservice (in separate terminals):
cd services/inventory-service
mvn spring-boot:run
cd services/payment-service
mvn spring-boot:run
cd services/fulfillment-service
mvn spring-boot:run
cd backend
mvn spring-boot:run
3. Start the frontend:
cd frontend
npm install
npm run dev
5.3. Access Points
Once everything is running:
-
React Frontend:
http://localhost:3000 -
OpenObserve Dashboard:
http://localhost:5080(root@example.com/ComplexPassword123) -
Order Service API:
http://localhost:8080/api/products -
Inventory Service:
http://localhost:8081/api/inventory/reserve -
Payment Service:
http://localhost:8082/api/payment/authorize -
Fulfillment Service:
http://localhost:8083/api/fulfillment/ship
Step 6: Analyzing Logs and Traces in OpenObserve
6.1. Viewing Logs
- Navigate to OpenObserve UI (
http://localhost:5080) - Click on the Logs tab
- Select the stream
default - Click Run Query
You'll see structured logs with:
- Log level (INFO, WARN, ERROR)
- Service name
- Message content
- Trace ID (for correlation)
- Span ID
- Timestamp
Structured logs from the Spring Boot microservices showing trace_id correlation
Log Correlation Example: Search for a specific trace_id to see all logs related to a single request:
trace_id = "your-trace-id-here"
6.2. Viewing Traces
- Navigate to the Traces tab
- Click Run Query to see all traces
Each trace shows:
- Service Map: Visual representation of service dependencies
- Gantt Chart: Time breakdown of each span
- Span Details: Tags, duration, status, and error information
- Trace ID: For cross-referencing with logs



Distributed trace showing the complete flow from order-service through all downstream services

Visual representation of service dependencies in the microservices architecture
6.3. Error Detection
The demo includes several failure simulation modes:
- Inventory Service Failure: Simulates DB row lock timeout (500 Internal Server Error)
- Payment Gateway Failure: Simulates card authorization decline (402 Payment Required)
- Fulfillment Service Failure: Simulates carrier API timeout (503 Service Unavailable)
- Database Connection Timeout: Simulates HikariCP pool exhaustion (504 Gateway Timeout)
Select any failure mode in the checkout UI to see how errors are captured in logs and traces.


Detailed view of a trace showing the exact span where an error occurred
Step 7: Error Simulation and Monitoring
7.1. Triggering Errors
In the React frontend, you can simulate different failure scenarios:
// In Checkout UI, select from dropdown:
// - INVENTORY: InventoryOutOfStockException
// - PAYMENT: PaymentGatewayDeclinedException
// - SHIPPING: CarrierServiceUnavailableException
// - DATABASE: DatabaseConnectionTimeoutException

Checkout page with failure mode selector for testing error scenarios
7.2. Error Response Example
When a payment failure occurs:
HTTP Response:
{
"success": false,
"message": "Order checkout failed: PaymentGatewayDeclinedException: Card authorization declined by issuing bank (Code 402)",
"traceId": "a1b2c3d4e5f6...",
"spanId": "g7h8i9j0...",
"data": {
"orderId": "ORD-1701234567890",
"status": "FAILED",
"failureReason": "PaymentGatewayDeclinedException: Card authorization declined by issuing bank (Code 402)"
}
}
7.3. Monitoring Errors
In OpenObserve Logs:
level: ERROR
message: "[payment-service] Payment authorization REJECTED for OrderID: ORD-1701234567890! Insufficient funds or invalid CVV."
trace_id: "a1b2c3d4e5f6..."
In OpenObserve Traces:
- The trace shows the exact span where the error occurred
- Status:
ERRORwith detailed error message - The entire request flow is visible, making it easy to pinpoint the failure point
Step 8: Key Features and Benefits
8.1. Multi-Hop Distributed Tracing
With W3C Trace Context headers, your traces automatically connect across service boundaries. Each HTTP call propagates the trace ID, creating a complete picture of request flow.
Services visible in OpenObserve:
order-serviceinventory-servicepayment-servicefulfillment-service
8.2. Live Log Correlation
Every log entry includes trace_id and span_id, allowing you to:
- Filter logs by trace ID to see all logs from a single request
- Jump from trace to logs and back for deeper investigation
- Understand the timing and sequence of operations
8.3. Structured Logging
Logs are sent as JSON, enabling:
- Easy filtering and searching
- Aggregation and analysis
- Integration with alerting systems
8.4. Cost-Effective Observability
OpenObserve is:
- Open-source (free to use)
- Efficient (compresses data)
- Scalable (handles millions of events)
8.5. Unified Dashboard
View logs, traces, and metrics in one place:
- No context switching between tools
- Full visibility into application health
- Faster incident response
Conclusion
Monitoring Spring Boot applications with OpenObserve is straightforward and provides powerful insights into your application's behavior. By implementing distributed tracing and structured logging, you gain:
- End-to-end visibility across your entire system
- Faster debugging with correlated logs and traces
- Better performance through bottleneck identification
- Cost-effective observability without vendor lock-in
The demo application and code provided in this guide give you a complete, working example that you can adapt for your own Spring Boot applications. Whether you're running a simple microservice or a complex distributed system, OpenObserve provides the observability you need to build reliable, maintainable applications.
Resources
- GitHub Repository: https://github.com/abhishek-dhnma/OpenObserve-Demo-with-Springboot
- Video Tutorial: https://youtu.be/fixVzjRBbcE
- OpenObserve Documentation: https://openobserve.ai/docs/
- OpenTelemetry Java: https://opentelemetry.io/docs/instrumentation/java/
- Spring Boot Actuator: https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html



Top comments (0)