DEV Community

Abhishek Dhiman
Abhishek Dhiman

Posted on

Monitoring Spring Boot Applications with OpenObserve: A Comprehensive Guide to Logs and Traces

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)  │   └────────────────┘   └─────────────────────┘
└──────────────┘
Enter fullscreen mode Exit fullscreen mode

The stack consists of 4 independent Spring Boot microservices:

  1. order-service (localhost:8080): Primary API Gateway & Order Checkout Coordinator
  2. inventory-service (localhost:8081): Stock management
  3. payment-service (localhost:8082): Payment processing
  4. 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:
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 Screen
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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Note: The Authorization header uses the Base64-encoded credentials. The format is Basic 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());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

5.2. Manual Start

If you prefer to start services manually:

1. Start OpenObserve:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

2. Start each microservice (in separate terminals):

cd services/inventory-service
mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode
cd services/payment-service
mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode
cd services/fulfillment-service
mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode
cd backend
mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode

3. Start the frontend:

cd frontend
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

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

  1. Navigate to OpenObserve UI (http://localhost:5080)
  2. Click on the Logs tab
  3. Select the stream default
  4. 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

OpenObserve Logs Dashboard

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"
Enter fullscreen mode Exit fullscreen mode

OpenObserve Logs Dashboard with trace id 1
OpenObserve Logs Dashboard with trace id 2

6.2. Viewing Traces

  1. Navigate to the Traces tab
  2. 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

OpenObserve Traces Dashboard 1
OpenObserve Traces Dashboard 2
OpenObserve Traces Dashboard 3
Distributed trace showing the complete flow from order-service through all downstream services

OpenObserve Service Map
Visual representation of service dependencies in the microservices architecture

6.3. Error Detection

The demo includes several failure simulation modes:

  1. Inventory Service Failure: Simulates DB row lock timeout (500 Internal Server Error)
  2. Payment Gateway Failure: Simulates card authorization decline (402 Payment Required)
  3. Fulfillment Service Failure: Simulates carrier API timeout (503 Service Unavailable)
  4. 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.

Trace with Error Details 1
Trace with Error Details 2
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
Enter fullscreen mode Exit fullscreen mode

Checkout with Error Simulation
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)"
  }
}
Enter fullscreen mode Exit fullscreen mode

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..."
Enter fullscreen mode Exit fullscreen mode

In OpenObserve Traces:

  • The trace shows the exact span where the error occurred
  • Status: ERROR with 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-service
  • inventory-service
  • payment-service
  • fulfillment-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

Top comments (0)