Skip to content
Last9 named a Gartner Cool Vendor in AI for SRE Observability for 2025! Read more →
Last9

Tomcat

Learn how to integrate OpenTelemetry with Apache Tomcat applications and send telemetry data to Last9

This guide shows you how to instrument your Apache Tomcat web application with OpenTelemetry and send traces, metrics, and logs to Last9.

Prerequisites

  • Java 8 or later (Java 11+ recommended)
  • Apache Tomcat 8.5+ or Tomcat 9.x/10.x
  • Last9 account with OTLP endpoint configured

Installation Methods

Choose between auto-instrumentation (recommended) or manual instrumentation:

  1. Download OpenTelemetry Java Agent

    For Java 11+:

    curl -L https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar -o opentelemetry-javaagent.jar

    For Java 8:

    curl -L https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.32.0/opentelemetry-javaagent.jar -o opentelemetry-javaagent.jar
  2. Place the agent in Tomcat directory

    # Copy to Tomcat lib directory
    cp opentelemetry-javaagent.jar $TOMCAT_HOME/lib/
    # Or create a dedicated directory
    mkdir -p $TOMCAT_HOME/otel
    cp opentelemetry-javaagent.jar $TOMCAT_HOME/otel/
  3. Configure Tomcat with OpenTelemetry

    Create or modify $TOMCAT_HOME/bin/setenv.sh (Linux/Mac) or setenv.bat (Windows):

    # setenv.sh
    export CATALINA_OPTS="$CATALINA_OPTS -javaagent:$TOMCAT_HOME/lib/opentelemetry-javaagent.jar"
    # OpenTelemetry Configuration
    export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"
    export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"
    export OTEL_SERVICE_NAME="tomcat-app"
    export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0,server.type=tomcat"
    export OTEL_TRACES_EXPORTER=otlp
    export OTEL_METRICS_EXPORTER=otlp
    export OTEL_LOGS_EXPORTER=otlp
    export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
    export OTEL_TRACES_SAMPLER=always_on
    # JVM Settings
    export CATALINA_OPTS="$CATALINA_OPTS -Xmx1024m -Xms512m"
    export CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC"

    For Windows (setenv.bat):

    set CATALINA_OPTS=%CATALINA_OPTS% -javaagent:%TOMCAT_HOME%\lib\opentelemetry-javaagent.jar
    set OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint
    set OTEL_EXPORTER_OTLP_HEADERS=Authorization=$last9_otlp_auth_header
    set OTEL_SERVICE_NAME=tomcat-app
    set OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0,server.type=tomcat
    set OTEL_TRACES_EXPORTER=otlp
    set OTEL_METRICS_EXPORTER=otlp
    set OTEL_LOGS_EXPORTER=otlp
    set OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
    set OTEL_TRACES_SAMPLER=always_on
  4. Start Tomcat

    $TOMCAT_HOME/bin/startup.sh
    # Or on Windows
    %TOMCAT_HOME%\bin\startup.bat

Manual Instrumentation

For more control over instrumentation, you can manually integrate OpenTelemetry in your web application:

  1. Add OpenTelemetry dependencies to your WAR project

    <properties>
    <opentelemetry.version>1.32.0</opentelemetry.version>
    <opentelemetry-instrumentation.version>1.32.0</opentelemetry-instrumentation.version>
    </properties>
    <dependencies>
    <!-- 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>
    <!-- OTLP Exporters -->
    <dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
    <version>${opentelemetry.version}</version>
    </dependency>
    <!-- Servlet instrumentation -->
    <dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-servlet-3.0</artifactId>
    <version>${opentelemetry-instrumentation.version}</version>
    </dependency>
    <!-- HTTP client instrumentations -->
    <dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-apache-httpclient-4.3</artifactId>
    <version>${opentelemetry-instrumentation.version}</version>
    </dependency>
    <!-- Database instrumentations -->
    <dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-jdbc</artifactId>
    <version>${opentelemetry-instrumentation.version}</version>
    </dependency>
    <!-- Servlet API -->
    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
    </dependency>
    <!-- JSON processing -->
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version>
    </dependency>
    </dependencies>
  2. Create OpenTelemetry Configuration

    package com.example.config;
    import io.opentelemetry.api.GlobalOpenTelemetry;
    import io.opentelemetry.api.OpenTelemetry;
    import io.opentelemetry.api.common.Attributes;
    import io.opentelemetry.api.trace.Tracer;
    import io.opentelemetry.context.propagation.ContextPropagators;
    import io.opentelemetry.context.propagation.TextMapPropagator;
    import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter;
    import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter;
    import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
    import io.opentelemetry.extension.trace.propagation.B3Propagator;
    import io.opentelemetry.extension.trace.propagation.JaegerPropagator;
    import io.opentelemetry.sdk.OpenTelemetrySdk;
    import io.opentelemetry.sdk.logs.LogRecordProcessor;
    import io.opentelemetry.sdk.logs.SdkLoggerProvider;
    import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
    import io.opentelemetry.sdk.metrics.SdkMeterProvider;
    import io.opentelemetry.sdk.metrics.export.MetricExporter;
    import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
    import io.opentelemetry.sdk.resources.Resource;
    import io.opentelemetry.sdk.trace.SdkTracerProvider;
    import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
    import io.opentelemetry.sdk.trace.export.SpanExporter;
    import io.opentelemetry.sdk.trace.samplers.Sampler;
    import io.opentelemetry.semconv.ResourceAttributes;
    import java.time.Duration;
    public class OpenTelemetryConfig {
    private static final String SERVICE_NAME = "tomcat-app";
    private static OpenTelemetry openTelemetry;
    public static void initialize() {
    if (openTelemetry != null) {
    return; // Already initialized
    }
    String otlpEndpoint = System.getenv("OTEL_EXPORTER_OTLP_ENDPOINT");
    String authHeader = System.getenv("OTEL_EXPORTER_OTLP_HEADERS");
    if (otlpEndpoint == null) {
    System.err.println("OTEL_EXPORTER_OTLP_ENDPOINT environment variable is not set");
    return;
    }
    // Extract authorization header
    String authorization = "";
    if (authHeader != null && authHeader.startsWith("Authorization=")) {
    authorization = authHeader.substring("Authorization=".length());
    }
    // Create resource
    Resource resource = Resource.getDefault()
    .merge(Resource.create(Attributes.of(
    ResourceAttributes.SERVICE_NAME, SERVICE_NAME,
    ResourceAttributes.SERVICE_VERSION, "1.0.0",
    ResourceAttributes.DEPLOYMENT_ENVIRONMENT,
    System.getProperty("deployment.environment", "production")
    )));
    // Configure exporters
    SpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
    .setEndpoint(otlpEndpoint + "/v1/traces")
    .addHeader("authorization", authorization)
    .setTimeout(Duration.ofSeconds(30))
    .build();
    MetricExporter metricExporter = OtlpGrpcMetricExporter.builder()
    .setEndpoint(otlpEndpoint + "/v1/metrics")
    .addHeader("authorization", authorization)
    .setTimeout(Duration.ofSeconds(30))
    .build();
    OtlpGrpcLogRecordExporter logExporter = OtlpGrpcLogRecordExporter.builder()
    .setEndpoint(otlpEndpoint + "/v1/logs")
    .addHeader("authorization", authorization)
    .setTimeout(Duration.ofSeconds(30))
    .build();
    // Create providers
    SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
    .addSpanProcessor(BatchSpanProcessor.builder(spanExporter)
    .setMaxExportBatchSize(512)
    .setExportTimeout(Duration.ofSeconds(30))
    .setScheduleDelay(Duration.ofSeconds(5))
    .build())
    .setResource(resource)
    .setSampler(Sampler.alwaysOn())
    .build();
    SdkMeterProvider meterProvider = SdkMeterProvider.builder()
    .registerMetricReader(PeriodicMetricReader.builder(metricExporter)
    .setInterval(Duration.ofSeconds(30))
    .build())
    .setResource(resource)
    .build();
    LogRecordProcessor logProcessor = BatchLogRecordProcessor.builder(logExporter)
    .setMaxExportBatchSize(512)
    .setExportTimeout(Duration.ofSeconds(30))
    .setScheduleDelay(Duration.ofSeconds(5))
    .build();
    SdkLoggerProvider loggerProvider = SdkLoggerProvider.builder()
    .addLogRecordProcessor(logProcessor)
    .setResource(resource)
    .build();
    // Configure context propagators
    ContextPropagators propagators = ContextPropagators.create(
    TextMapPropagator.composite(
    TextMapPropagator.composite(),
    B3Propagator.injectingSingleHeader(),
    JaegerPropagator.getInstance()
    )
    );
    // Build OpenTelemetry SDK
    openTelemetry = OpenTelemetrySdk.builder()
    .setTracerProvider(tracerProvider)
    .setMeterProvider(meterProvider)
    .setLoggerProvider(loggerProvider)
    .setPropagators(propagators)
    .build();
    // Set as global instance
    GlobalOpenTelemetry.set(openTelemetry);
    System.out.println("OpenTelemetry initialized successfully for Tomcat application");
    }
    public static OpenTelemetry getOpenTelemetry() {
    if (openTelemetry == null) {
    initialize();
    }
    return openTelemetry;
    }
    public static Tracer getTracer() {
    return getOpenTelemetry().getTracer(SERVICE_NAME, "1.0.0");
    }
    public static void shutdown() {
    if (openTelemetry instanceof OpenTelemetrySdk) {
    ((OpenTelemetrySdk) openTelemetry).close();
    }
    }
    }
  3. Create Servlet Filter for Request Tracing

    package com.example.filter;
    import com.example.config.OpenTelemetryConfig;
    import io.opentelemetry.api.common.Attributes;
    import io.opentelemetry.api.trace.Span;
    import io.opentelemetry.api.trace.SpanKind;
    import io.opentelemetry.api.trace.StatusCode;
    import io.opentelemetry.api.trace.Tracer;
    import io.opentelemetry.context.Context;
    import io.opentelemetry.context.Scope;
    import io.opentelemetry.context.propagation.TextMapGetter;
    import io.opentelemetry.semconv.SemanticAttributes;
    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Collections;
    import java.util.Enumeration;
    @WebFilter(urlPatterns = "/*")
    public class TracingFilter implements Filter {
    private static final TextMapGetter<HttpServletRequest> GETTER = new HttpServletRequestTextMapGetter();
    private Tracer tracer;
    @Override
    public void init(FilterConfig filterConfig) {
    this.tracer = OpenTelemetryConfig.getTracer();
    }
    @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 incoming request
    Context parentContext = OpenTelemetryConfig.getOpenTelemetry()
    .getPropagators()
    .getTextMapPropagator()
    .extract(Context.current(), httpRequest, GETTER);
    // Create span
    String spanName = httpRequest.getMethod() + " " + getRoute(httpRequest);
    Span span = tracer.spanBuilder(spanName)
    .setParent(parentContext)
    .setSpanKind(SpanKind.SERVER)
    .setAllAttributes(getRequestAttributes(httpRequest))
    .startSpan();
    long startTime = System.currentTimeMillis();
    try (Scope scope = span.makeCurrent()) {
    // Store span in request for access by servlets
    httpRequest.setAttribute("otel.span", span);
    chain.doFilter(request, response);
    // Set response attributes
    span.setAllAttributes(getResponseAttributes(httpResponse, System.currentTimeMillis() - startTime));
    span.setStatus(StatusCode.OK);
    } catch (Exception e) {
    span.recordException(e);
    span.setStatus(StatusCode.ERROR, e.getMessage());
    throw e;
    } finally {
    span.end();
    }
    }
    private Attributes getRequestAttributes(HttpServletRequest request) {
    return Attributes.builder()
    .put(SemanticAttributes.HTTP_METHOD, request.getMethod())
    .put(SemanticAttributes.HTTP_URL, request.getRequestURL().toString())
    .put(SemanticAttributes.HTTP_ROUTE, getRoute(request))
    .put(SemanticAttributes.HTTP_SCHEME, request.getScheme())
    .put(SemanticAttributes.HTTP_HOST, request.getHeader("Host"))
    .put(SemanticAttributes.HTTP_USER_AGENT, request.getHeader("User-Agent"))
    .put(SemanticAttributes.HTTP_CLIENT_IP, getClientIp(request))
    .put("http.request.size", getRequestSize(request))
    .build();
    }
    private Attributes getResponseAttributes(HttpServletResponse response, long durationMs) {
    return Attributes.builder()
    .put(SemanticAttributes.HTTP_STATUS_CODE, response.getStatus())
    .put("http.response.size", getResponseSize(response))
    .put("http.request.duration_ms", durationMs)
    .build();
    }
    private String getRoute(HttpServletRequest request) {
    String pathInfo = request.getPathInfo();
    if (pathInfo != null) {
    return request.getServletPath() + pathInfo;
    }
    return request.getServletPath();
    }
    private String getClientIp(HttpServletRequest request) {
    String xForwardedFor = request.getHeader("X-Forwarded-For");
    if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
    return xForwardedFor.split(",")[0].trim();
    }
    String xRealIp = request.getHeader("X-Real-IP");
    if (xRealIp != null && !xRealIp.isEmpty()) {
    return xRealIp;
    }
    return request.getRemoteAddr();
    }
    private long getRequestSize(HttpServletRequest request) {
    String contentLength = request.getHeader("Content-Length");
    if (contentLength != null) {
    try {
    return Long.parseLong(contentLength);
    } catch (NumberFormatException e) {
    return 0;
    }
    }
    return 0;
    }
    private long getResponseSize(HttpServletResponse response) {
    // Note: This might not be accurate for all response types
    return 0; // Tomcat doesn't easily expose response size
    }
    @Override
    public void destroy() {
    // Cleanup if needed
    }
    private static class HttpServletRequestTextMapGetter implements TextMapGetter<HttpServletRequest> {
    @Override
    public Iterable<String> keys(HttpServletRequest request) {
    return Collections.list(request.getHeaderNames());
    }
    @Override
    public String get(HttpServletRequest request, String key) {
    return request.getHeader(key);
    }
    }
    }

Servlet Examples

REST API Servlet

package com.example.servlet;
import com.example.config.OpenTelemetryConfig;
import com.example.model.User;
import com.example.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@WebServlet(urlPatterns = {"/api/users", "/api/users/*"})
public class UserServlet extends HttpServlet {
private UserService userService;
private ObjectMapper objectMapper;
private Tracer tracer;
@Override
public void init() throws ServletException {
this.userService = new UserService();
this.objectMapper = new ObjectMapper();
this.tracer = OpenTelemetryConfig.getTracer();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Span span = tracer.spanBuilder("user_servlet.do_get")
.setAllAttributes(Attributes.builder()
.put("servlet", "UserServlet")
.put("action", "doGet")
.build())
.startSpan();
try {
String pathInfo = request.getPathInfo();
if (pathInfo == null || pathInfo.equals("/")) {
// Get all users
handleGetAllUsers(request, response, span);
} else {
// Get user by ID
String[] pathParts = pathInfo.split("/");
if (pathParts.length == 2) {
try {
Long userId = Long.parseLong(pathParts[1]);
handleGetUserById(request, response, userId, span);
} catch (NumberFormatException e) {
sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
"Invalid user ID format", span);
}
} else {
sendErrorResponse(response, HttpServletResponse.SC_NOT_FOUND,
"Resource not found", span);
}
}
span.setStatus(StatusCode.OK);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Internal server error", span);
} finally {
span.end();
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Span span = tracer.spanBuilder("user_servlet.do_post")
.setAllAttributes(Attributes.builder()
.put("servlet", "UserServlet")
.put("action", "doPost")
.build())
.startSpan();
try {
// Parse request body
User user = objectMapper.readValue(request.getInputStream(), User.class);
span.setAllAttributes(Attributes.builder()
.put("user_email", user.getEmail())
.put("operation", "create_user")
.build());
// Create user
User createdUser = userService.createUser(user);
span.setAllAttributes(Attributes.builder()
.put("user_created", true)
.put("user_id", createdUser.getId())
.build());
// Send response
response.setStatus(HttpServletResponse.SC_CREATED);
response.setContentType("application/json");
objectMapper.writeValue(response.getOutputStream(), createdUser);
span.setStatus(StatusCode.OK);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Failed to create user", span);
} finally {
span.end();
}
}
@Override
protected void doPut(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Span span = tracer.spanBuilder("user_servlet.do_put")
.setAllAttributes(Attributes.builder()
.put("servlet", "UserServlet")
.put("action", "doPut")
.build())
.startSpan();
try {
String pathInfo = request.getPathInfo();
if (pathInfo == null) {
sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
"User ID is required", span);
return;
}
String[] pathParts = pathInfo.split("/");
if (pathParts.length != 2) {
sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
"Invalid path", span);
return;
}
Long userId = Long.parseLong(pathParts[1]);
User user = objectMapper.readValue(request.getInputStream(), User.class);
span.setAllAttributes(Attributes.builder()
.put("user_id", userId)
.put("user_email", user.getEmail())
.put("operation", "update_user")
.build());
// Update user
User updatedUser = userService.updateUser(userId, user);
if (updatedUser != null) {
span.setAllAttributes(Attributes.builder()
.put("user_updated", true)
.build());
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
objectMapper.writeValue(response.getOutputStream(), updatedUser);
} else {
span.setAllAttributes(Attributes.builder()
.put("user_found", false)
.build());
sendErrorResponse(response, HttpServletResponse.SC_NOT_FOUND,
"User not found", span);
}
span.setStatus(StatusCode.OK);
} catch (NumberFormatException e) {
span.recordException(e);
sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
"Invalid user ID format", span);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Failed to update user", span);
} finally {
span.end();
}
}
@Override
protected void doDelete(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Span span = tracer.spanBuilder("user_servlet.do_delete")
.setAllAttributes(Attributes.builder()
.put("servlet", "UserServlet")
.put("action", "doDelete")
.build())
.startSpan();
try {
String pathInfo = request.getPathInfo();
if (pathInfo == null) {
sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
"User ID is required", span);
return;
}
String[] pathParts = pathInfo.split("/");
if (pathParts.length != 2) {
sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
"Invalid path", span);
return;
}
Long userId = Long.parseLong(pathParts[1]);
span.setAllAttributes(Attributes.builder()
.put("user_id", userId)
.put("operation", "delete_user")
.build());
// Delete user
boolean deleted = userService.deleteUser(userId);
if (deleted) {
span.setAllAttributes(Attributes.builder()
.put("user_deleted", true)
.build());
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
} else {
span.setAllAttributes(Attributes.builder()
.put("user_found", false)
.build());
sendErrorResponse(response, HttpServletResponse.SC_NOT_FOUND,
"User not found", span);
}
span.setStatus(StatusCode.OK);
} catch (NumberFormatException e) {
span.recordException(e);
sendErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST,
"Invalid user ID format", span);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Failed to delete user", span);
} finally {
span.end();
}
}
private void handleGetAllUsers(HttpServletRequest request, HttpServletResponse response, Span span)
throws IOException {
// Get pagination parameters
int page = getIntParameter(request, "page", 0);
int size = getIntParameter(request, "size", 10);
span.setAllAttributes(Attributes.builder()
.put("operation", "get_all_users")
.put("page", page)
.put("size", size)
.build());
List<User> users = userService.getAllUsers(page, size);
span.setAllAttributes(Attributes.builder()
.put("users_count", users.size())
.build());
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
// Create response with pagination info
String jsonResponse = objectMapper.writeValueAsString(Map.of(
"users", users,
"pagination", Map.of(
"page", page,
"size", size,
"count", users.size()
)
));
response.getWriter().write(jsonResponse);
}
private void handleGetUserById(HttpServletRequest request, HttpServletResponse response,
Long userId, Span span) throws IOException {
span.setAllAttributes(Attributes.builder()
.put("operation", "get_user_by_id")
.put("user_id", userId)
.build());
User user = userService.getUserById(userId);
if (user != null) {
span.setAllAttributes(Attributes.builder()
.put("user_found", true)
.put("user_email", user.getEmail())
.build());
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
objectMapper.writeValue(response.getOutputStream(), user);
} else {
span.setAllAttributes(Attributes.builder()
.put("user_found", false)
.build());
sendErrorResponse(response, HttpServletResponse.SC_NOT_FOUND,
"User not found", span);
}
}
private void sendErrorResponse(HttpServletResponse response, int statusCode,
String message, Span span) throws IOException {
span.setAllAttributes(Attributes.builder()
.put("error", true)
.put("error_message", message)
.put("status_code", statusCode)
.build());
response.setStatus(statusCode);
response.setContentType("application/json");
String errorJson = objectMapper.writeValueAsString(Map.of(
"error", message,
"status", statusCode
));
response.getWriter().write(errorJson);
}
private int getIntParameter(HttpServletRequest request, String name, int defaultValue) {
String value = request.getParameter(name);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
return defaultValue;
}
}

Production Deployment

Docker Configuration

FROM tomcat:9.0-openjdk11-slim
# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Download OpenTelemetry Java agent
ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar /usr/local/tomcat/lib/opentelemetry-javaagent.jar
# Copy application WAR
COPY target/tomcat-app.war /usr/local/tomcat/webapps/
# Copy Tomcat configuration
COPY conf/server.xml /usr/local/tomcat/conf/
COPY conf/web.xml /usr/local/tomcat/conf/
# Set up OpenTelemetry environment
COPY setenv.sh /usr/local/tomcat/bin/
RUN chmod +x /usr/local/tomcat/bin/setenv.sh
# Create non-root user
RUN groupadd -r tomcat && useradd -r -g tomcat tomcat
RUN chown -R tomcat:tomcat /usr/local/tomcat
USER tomcat
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/tomcat-app/health || exit 1
# Start Tomcat
CMD ["catalina.sh", "run"]

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
name: tomcat-app
labels:
app: tomcat-app
spec:
replicas: 2
selector:
matchLabels:
app: tomcat-app
template:
metadata:
labels:
app: tomcat-app
spec:
containers:
- name: tomcat-app
image: your-registry/tomcat-app:latest
ports:
- containerPort: 8080
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "$last9_otlp_endpoint"
- name: OTEL_EXPORTER_OTLP_HEADERS
value: "Authorization=$last9_otlp_auth_header"
- name: OTEL_SERVICE_NAME
value: "tomcat-app"
- name: OTEL_RESOURCE_ATTRIBUTES
value: "deployment.environment=production,service.version=1.0.0,server.type=tomcat,k8s.cluster.name=production"
- name: DB_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
livenessProbe:
httpGet:
path: /tomcat-app/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /tomcat-app/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
---
apiVersion: v1
kind: Service
metadata:
name: tomcat-app-service
spec:
selector:
app: tomcat-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer

Testing the Integration

  1. Start Tomcat

    # Direct start
    $TOMCAT_HOME/bin/startup.sh
    # Or with Docker
    docker-compose up
  2. Test the endpoints

    # Health check
    curl http://localhost:8080/tomcat-app/health
    # User endpoints
    curl http://localhost:8080/tomcat-app/api/users
    curl http://localhost:8080/tomcat-app/api/users/1
    # Create a user
    curl -X POST http://localhost:8080/tomcat-app/api/users \
    -H "Content-Type: application/json" \
    -d '{"name": "Test User", "email": "test@example.com"}'
  3. View telemetry in Last9

    Check your Last9 dashboard for:

    • HTTP request traces with Tomcat-specific attributes
    • Servlet execution spans
    • Database operation traces
    • Custom business logic spans
    • Error tracking and correlation