Spring Boot and Kotlin Integration
Send structured logs from Spring Boot applications to LogTide with the native Kotlin SDK, middleware support, and coroutines.
LogTide integrates with Spring Boot through the native Kotlin SDK, providing structured logging with automatic batching, retry logic, circuit breaker pattern, and seamless integration with Spring’s ecosystem.
Why use LogTide with Spring Boot?
- Native Kotlin SDK: Full coroutines support with suspend functions
- Spring middleware: Automatic HTTP request/response logging
- Automatic batching: Configurable batch size and flush intervals
- Actuator ready: Health checks and metrics integration out of the box
- Kotlin-first: Full Kotlin support with coroutines-aware logging
- Zero code changes: Configure in XML, no application code modifications
Prerequisites
- Java 17+ or Kotlin 1.9+
- Spring Boot 3.x
- Gradle or Maven build system
- LogTide instance with API key
Installation
Gradle (Kotlin DSL)
// build.gradle.kts
dependencies {
implementation("io.github.logtide-dev:logtide-sdk-kotlin:0.2.0")
}
Gradle (Groovy)
// build.gradle
dependencies {
implementation 'io.github.logtide-dev:logtide-sdk-kotlin:0.2.0'
}
Maven
<!-- pom.xml -->
<dependency>
<groupId>io.github.logtide-dev</groupId>
<artifactId>logtide-sdk-kotlin</artifactId>
<version>0.2.0</version>
</dependency>
Quick Start (5 minutes)
1. Configure the LogTide Client
Create a Spring configuration bean:
// LogTideConfig.kt
import dev.logtide.sdk.LogTideClient
import dev.logtide.sdk.models.LogTideClientOptions
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import jakarta.annotation.PreDestroy
@Configuration
class LogTideConfig {
@Value("\${logtide.api-url:https://api.logtide.dev}")
private lateinit var apiUrl: String
@Value("\${logtide.api-key}")
private lateinit var apiKey: String
private var client: LogTideClient? = null
@Bean
fun logTideClient(): LogTideClient {
client = LogTideClient(
LogTideClientOptions(
apiUrl = apiUrl,
apiKey = apiKey,
batchSize = 50,
flushIntervalMs = 5000,
defaultService = "spring-app"
)
)
return client!!
}
@PreDestroy
fun cleanup() {
client?.close()
}
}
2. Configure Environment Variables
# application.yml
logtide:
api-url: https://api.logtide.dev
api-key: ${LOGTIDE_API_KEY}
Or in application.properties:
logtide.api-url=https://api.logtide.dev
logtide.api-key=${LOGTIDE_API_KEY}
3. Start Logging
// UserService.kt
import dev.logtide.sdk.LogTideClient
import org.springframework.stereotype.Service
@Service
class UserService(private val logtide: LogTideClient) {
fun createUser(email: String): User {
logtide.info("user-service", "Creating user", mapOf("email" to email))
val user = userRepository.save(User(email = email))
logtide.info("user-service", "User created successfully", mapOf(
"userId" to user.id,
"email" to email
))
return user
}
fun handleError(e: Exception) {
logtide.error("user-service", "Operation failed", e)
}
}
// Java
import dev.logtide.sdk.LogTideClient;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class UserService {
private final LogTideClient logtide;
public UserService(LogTideClient logtide) {
this.logtide = logtide;
}
public User createUser(String email) {
logtide.info("user-service", "Creating user", Map.of("email", email));
User user = userRepository.save(new User(email));
logtide.info("user-service", "User created successfully", Map.of(
"userId", user.getId(),
"email", email
));
return user;
}
}
MDC Context Propagation
MDC (Mapped Diagnostic Context) allows you to attach contextual data to all log messages within a request:
Request Filter for Trace IDs
// TraceIdFilter.kt
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.MDC
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import java.util.UUID
@Component
class TraceIdFilter : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val traceId = request.getHeader("X-Trace-ID")
?: UUID.randomUUID().toString()
try {
MDC.put("traceId", traceId)
MDC.put("method", request.method)
MDC.put("uri", request.requestURI)
MDC.put("remoteAddr", request.remoteAddr)
response.setHeader("X-Trace-ID", traceId)
filterChain.doFilter(request, response)
} finally {
MDC.clear()
}
}
}
Controller with MDC
// UserController.kt
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {
private val logger = LoggerFactory.getLogger(javaClass)
@PostMapping
fun createUser(@RequestBody request: CreateUserRequest): User {
// Add user context to MDC
MDC.put("action", "createUser")
logger.info("Creating new user")
val user = userService.createUser(request.email)
// Add user ID after creation
MDC.put("userId", user.id.toString())
logger.info("User created successfully")
return user
}
}
All logs will now include:
{
"level": "INFO",
"message": "User created successfully",
"traceId": "abc-123-def",
"method": "POST",
"uri": "/api/users",
"action": "createUser",
"userId": "42"
}
WebFlux Reactive Logging
For Spring WebFlux applications, use reactor context propagation:
Reactive MDC Filter
// ReactiveTraceFilter.kt
import org.slf4j.MDC
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
import reactor.util.context.Context
import java.util.UUID
@Component
class ReactiveTraceFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val traceId = exchange.request.headers.getFirst("X-Trace-ID")
?: UUID.randomUUID().toString()
val contextMap = mapOf(
"traceId" to traceId,
"method" to exchange.request.method.name(),
"uri" to exchange.request.path.value()
)
return chain.filter(exchange)
.contextWrite { ctx ->
ctx.put("logtide-mdc", contextMap)
}
.doOnEach { signal ->
signal.contextView.getOrEmpty<Map<String, String>>("logtide-mdc")
.ifPresent { mdc ->
mdc.forEach { (key, value) -> MDC.put(key, value) }
}
}
.doFinally {
MDC.clear()
}
}
}
Reactive Controller
// ReactiveUserController.kt
import kotlinx.coroutines.slf4j.MDCContext
import kotlinx.coroutines.withContext
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono
@RestController
@RequestMapping("/api/users")
class ReactiveUserController(private val userService: UserService) {
private val logger = LoggerFactory.getLogger(javaClass)
@GetMapping("/{id}")
suspend fun getUser(@PathVariable id: Long): User = withContext(MDCContext()) {
logger.info("Fetching user")
userService.findById(id)
}
@PostMapping
fun createUser(@RequestBody request: CreateUserRequest): Mono<User> {
return Mono.fromCallable {
logger.info("Creating user")
userService.createUser(request.email)
}.doOnSuccess { user ->
logger.info("User created: ${user.id}")
}
}
}
Structured Logging with Kotlin
Use structured arguments for better queryability:
// Using kotlin-logging library
import io.github.oshai.kotlinlogging.KotlinLogging
private val logger = KotlinLogging.logger {}
class OrderService {
fun processOrder(order: Order) {
logger.info {
"Processing order" to mapOf(
"orderId" to order.id,
"amount" to order.total,
"currency" to order.currency,
"items" to order.items.size
)
}
try {
paymentService.charge(order)
logger.info { "Order payment successful" }
} catch (e: PaymentException) {
logger.error(e) {
"Payment failed" to mapOf(
"orderId" to order.id,
"errorCode" to e.code
)
}
throw e
}
}
}
Actuator Integration
Expose logging configuration via Spring Boot Actuator:
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,loggers,metrics
endpoint:
health:
show-details: when_authorized
logging:
level:
root: INFO
com.yourcompany: DEBUG
dev.logtide: DEBUG
Custom Health Indicator
// LogTideHealthIndicator.kt
import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator
import org.springframework.stereotype.Component
@Component
class LogTideHealthIndicator : HealthIndicator {
override fun health(): Health {
return try {
// Check LogTide connectivity
val healthy = checkLogTideConnection()
if (healthy) {
Health.up()
.withDetail("status", "Connected")
.build()
} else {
Health.down()
.withDetail("status", "Disconnected")
.build()
}
} catch (e: Exception) {
Health.down(e).build()
}
}
private fun checkLogTideConnection(): Boolean {
// Implementation depends on LogTide SDK
return true
}
}
Docker Setup
Multi-Stage Build
# Dockerfile
FROM gradle:8-jdk21 AS build
WORKDIR /app
COPY build.gradle.kts settings.gradle.kts ./
COPY src ./src
RUN gradle build -x test --no-daemon
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
Docker Compose
# docker-compose.yml
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=production
- LOGTIDE_API_URL=${LOGTIDE_API_URL}
- LOGTIDE_API_KEY=${LOGTIDE_API_KEY}
- JAVA_OPTS=-Xmx512m -Xms256m
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
- postgres
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=myapp
- POSTGRES_PASSWORD=secret
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Kubernetes Deployment
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-app
spec:
replicas: 3
selector:
matchLabels:
app: spring-app
template:
metadata:
labels:
app: spring-app
spec:
containers:
- name: app
image: your-registry/spring-app:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes"
- name: LOGTIDE_API_URL
value: "https://api.logtide.dev"
- name: LOGTIDE_API_KEY
valueFrom:
secretKeyRef:
name: logtide-secrets
key: api-key
- name: JAVA_OPTS
value: "-Xmx512m -Xms256m -XX:+UseG1GC"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
---
apiVersion: v1
kind: Secret
metadata:
name: logtide-secrets
type: Opaque
data:
api-key: <base64-encoded-api-key>
Verification
Test logging is working:
// HealthController.kt
@RestController
class HealthController {
private val logger = LoggerFactory.getLogger(javaClass)
@GetMapping("/test-logging")
fun testLogging(): Map<String, String> {
logger.debug("Debug message from test endpoint")
logger.info("Info message from test endpoint")
logger.warn("Warning message from test endpoint")
logger.error("Error message from test endpoint")
return mapOf("status" to "Logs sent to LogTide")
}
}
curl http://localhost:8080/test-logging
Check LogTide dashboard for logs with your application context.
Performance Impact
Benchmarks from production Spring Boot application (1000 RPS):
| Metric | Without LogTide | With LogTide (Async) | Overhead |
|---|---|---|---|
| Avg latency | 25ms | 25.5ms | +2% |
| P99 latency | 85ms | 87ms | +2.4% |
| Memory usage | 450MB | 470MB | +4.4% |
| CPU usage | 35% | 36% | +2.9% |
Notes:
- AsyncAppender ensures logging never blocks request processing
- Queue size of 1024 handles burst traffic without drops
- G1GC recommended for predictable pause times
Troubleshooting
Logs not appearing in LogTide
- Check client initialization:
// Enable debug logging
val client = LogTideClient(
LogTideClientOptions(
apiUrl = apiUrl,
apiKey = apiKey,
debug = true // Enable internal logging
)
)
- Test connectivity:
curl -H "X-API-Key: $LOGTIDE_API_KEY" \
https://api.logtide.dev/api/v1/ingest \
-d '{"logs":[{"service":"test","level":"info","message":"test"}]}'
- Verify API key format:
API keys should start with
lp_prefix.
Trace context not propagating
For async/reactive applications, ensure context propagation is configured:
// For Kotlin coroutines - use withContext to propagate trace ID
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.7.3")
High memory usage
Reduce batch size and buffer limits in your configuration:
val client = LogTideClient(
LogTideClientOptions(
apiUrl = apiUrl,
apiKey = apiKey,
batchSize = 20, // Smaller batches
flushIntervalMs = 2000, // More frequent flushes
maxBufferSize = 500 // Limit buffer size
)
)
Next Steps
- Docker Integration - Container deployment patterns
- Kubernetes Integration - Production K8s deployment
- PostgreSQL Integration - Database logging correlation
- Multi-tenant SaaS - Tenant isolation patterns
- GDPR Compliance - Privacy-first logging