Introduction to LogQL and Loki Integration
LogQL is Grafana Loki's query language that enables powerful log aggregation and analysis. This guide covers comprehensive Java integration with Loki, including query execution, log processing, and building monitoring applications.
Table of Contents
- Loki Client Setup and Configuration
- LogQL Query Execution
- Log Stream Processing
- Metrics Extraction
- Alerting and Monitoring
- Performance Optimization
- Spring Boot Integration
- Real-World Use Cases
1. Project Setup and Dependencies
Maven Configuration
<properties>
<loki4j.version>1.4.0</loki4j.version>
<okhttp.version>4.11.0</okhttp.version>
<spring-boot.version>3.1.0</spring-boot.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Loki Java Client -->
<dependency>
<groupId>com.github.loki4j</groupId>
<artifactId>loki4j-client</artifactId>
<version>${loki4j.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.13.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Application Configuration
# application.yml
loki:
url: http://localhost:3100
timeout: 30000
retry:
max-attempts: 3
backoff-ms: 1000
auth:
type: none # basic, bearer, none
username: ${LOKI_USERNAME:}
password: ${LOKI_PASSWORD:}
token: ${LOKI_TOKEN:}
app:
logging:
loki:
enabled: true
batch-size: 100
batch-timeout-ms: 1000
labels:
application: order-service
environment: ${ENVIRONMENT:development}
instance: ${HOSTNAME:localhost}
management:
endpoints:
web:
exposure:
include: health,info,metrics,loki
endpoint:
health:
show-details: always
2. Loki Client Implementation
Loki Client Configuration
package com.example.loki.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Configuration
public class LokiConfig {
@Value("${loki.url:http://localhost:3100}")
private String lokiUrl;
@Value("${loki.timeout:30000}")
private long timeout;
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(Duration.ofMillis(timeout))
.readTimeout(Duration.ofMillis(timeout))
.writeTimeout(Duration.ofMillis(timeout))
.retryOnConnectionFailure(true)
.build();
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public LokiClient lokiClient(OkHttpClient okHttpClient, ObjectMapper objectMapper) {
return new LokiClient(lokiUrl, okHttpClient, objectMapper);
}
}
Core Loki Client
package com.example.loki.client;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Component
public class LokiClient {
private static final Logger logger = LoggerFactory.getLogger(LokiClient.class);
private final String lokiBaseUrl;
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
@Value("${loki.auth.type:none}")
private String authType;
@Value("${loki.auth.username:}")
private String username;
@Value("${loki.auth.password:}")
private String password;
@Value("${loki.auth.token:}")
private String token;
public LokiClient(String lokiBaseUrl, OkHttpClient httpClient, ObjectMapper objectMapper) {
this.lokiBaseUrl = lokiBaseUrl.endsWith("/") ?
lokiBaseUrl.substring(0, lokiBaseUrl.length() - 1) : lokiBaseUrl;
this.httpClient = httpClient;
this.objectMapper = objectMapper;
}
// Query range - most common LogQL query
public LokiQueryResponse queryRange(String query, long start, long end,
Long limit, String direction) {
HttpUrl.Builder urlBuilder = HttpUrl.parse(lokiBaseUrl + "/loki/api/v1/query_range")
.newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("start", String.valueOf(start))
.addQueryParameter("end", String.valueOf(end));
if (limit != null) {
urlBuilder.addQueryParameter("limit", String.valueOf(limit));
}
if (direction != null) {
urlBuilder.addQueryParameter("direction", direction);
}
Request request = buildRequest(urlBuilder.build());
return executeRequest(request, LokiQueryResponse.class);
}
// Instant query
public LokiQueryResponse query(String query, Long limit, String time) {
HttpUrl.Builder urlBuilder = HttpUrl.parse(lokiBaseUrl + "/loki/api/v1/query")
.newBuilder()
.addQueryParameter("query", query);
if (limit != null) {
urlBuilder.addQueryParameter("limit", String.valueOf(limit));
}
if (time != null) {
urlBuilder.addQueryParameter("time", time);
}
Request request = buildRequest(urlBuilder.build());
return executeRequest(request, LokiQueryResponse.class);
}
// Labels query
public List<String> labels(String start, String end) {
HttpUrl.Builder urlBuilder = HttpUrl.parse(lokiBaseUrl + "/loki/api/v1/labels")
.newBuilder();
if (start != null) {
urlBuilder.addQueryParameter("start", start);
}
if (end != null) {
urlBuilder.addQueryParameter("end", end);
}
Request request = buildRequest(urlBuilder.build());
LokiLabelsResponse response = executeRequest(request, LokiLabelsResponse.class);
return response != null ? response.getData() : Collections.emptyList();
}
// Label values query
public List<String> labelValues(String label, String start, String end) {
HttpUrl.Builder urlBuilder = HttpUrl.parse(lokiBaseUrl + "/loki/api/v1/label/" + label + "/values")
.newBuilder();
if (start != null) {
urlBuilder.addQueryParameter("start", start);
}
if (end != null) {
urlBuilder.addQueryParameter("end", end);
}
Request request = buildRequest(urlBuilder.build());
LokiLabelsResponse response = executeRequest(request, LokiLabelsResponse.class);
return response != null ? response.getData() : Collections.emptyList();
}
// Series query
public List<Map<String, String>> series(String match, String start, String end) {
HttpUrl.Builder urlBuilder = HttpUrl.parse(lokiBaseUrl + "/loki/api/v1/series")
.newBuilder();
if (match != null && !match.isEmpty()) {
for (String m : match.split(",")) {
urlBuilder.addQueryParameter("match[]", m.trim());
}
}
if (start != null) {
urlBuilder.addQueryParameter("start", start);
}
if (end != null) {
urlBuilder.addQueryParameter("end", end);
}
Request request = buildRequest(urlBuilder.build());
LokiSeriesResponse response = executeRequest(request, LokiSeriesResponse.class);
return response != null ? response.getData() : Collections.emptyList();
}
// Push logs to Loki
public boolean pushLogs(LokiPushRequest pushRequest) {
try {
String json = objectMapper.writeValueAsString(pushRequest);
RequestBody body = RequestBody.create(
json, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(lokiBaseUrl + "/loki/api/v1/push")
.post(body)
.build();
request = addAuthHeaders(request);
try (Response response = httpClient.newCall(request).execute()) {
if (response.isSuccessful()) {
logger.debug("Logs pushed successfully to Loki");
return true;
} else {
logger.error("Failed to push logs to Loki: {} - {}",
response.code(), response.message());
return false;
}
}
} catch (Exception e) {
logger.error("Error pushing logs to Loki", e);
return false;
}
}
private Request buildRequest(HttpUrl url) {
return addAuthHeaders(new Request.Builder()
.url(url)
.get()
.build());
}
private Request addAuthHeaders(Request request) {
Request.Builder builder = request.newBuilder();
switch (authType.toLowerCase()) {
case "basic":
if (!username.isEmpty() && !password.isEmpty()) {
String credentials = Credentials.basic(username, password);
builder.header("Authorization", credentials);
}
break;
case "bearer":
if (!token.isEmpty()) {
builder.header("Authorization", "Bearer " + token);
}
break;
case "none":
default:
// No authentication
break;
}
return builder.build();
}
private <T> T executeRequest(Request request, Class<T> responseType) {
try (Response response = httpClient.newCall(request).execute()) {
if (response.isSuccessful()) {
ResponseBody body = response.body();
if (body != null) {
return objectMapper.readValue(body.string(), responseType);
}
} else {
logger.error("Loki API request failed: {} - {}",
response.code(), response.message());
if (response.body() != null) {
logger.error("Response body: {}", response.body().string());
}
}
} catch (IOException e) {
logger.error("Error executing Loki API request", e);
}
return null;
}
// Response classes
public static class LokiQueryResponse {
private String status;
private QueryData data;
// Getters and setters
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public QueryData getData() { return data; }
public void setData(QueryData data) { this.data = data; }
public static class QueryData {
private String resultType;
private List<StreamResult> result;
public String getResultType() { return resultType; }
public void setResultType(String resultType) { this.resultType = resultType; }
public List<StreamResult> getResult() { return result; }
public void setResult(List<StreamResult> result) { this.result = result; }
}
public static class StreamResult {
private Map<String, String> stream;
private List<List<String>> values; // [[timestamp, logline], ...]
public Map<String, String> getStream() { return stream; }
public void setStream(Map<String, String> stream) { this.stream = stream; }
public List<List<String>> getValues() { return values; }
public void setValues(List<List<String>> values) { this.values = values; }
}
}
public static class LokiLabelsResponse {
private String status;
private List<String> data;
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public List<String> getData() { return data; }
public void setData(List<String> data) { this.data = data; }
}
public static class LokiSeriesResponse {
private String status;
private List<Map<String, String>> data;
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public List<Map<String, String>> getData() { return data; }
public void setData(List<Map<String, String>> data) { this.data = data; }
}
public static class LokiPushRequest {
private List<Stream> streams;
public LokiPushRequest(List<Stream> streams) {
this.streams = streams;
}
public List<Stream> getStreams() { return streams; }
public void setStreams(List<Stream> streams) { this.streams = streams; }
public static class Stream {
private Map<String, String> stream;
private List<List<String>> values;
public Stream(Map<String, String> stream, List<List<String>> values) {
this.stream = stream;
this.values = values;
}
public Map<String, String> getStream() { return stream; }
public void setStream(Map<String, String> stream) { this.stream = stream; }
public List<List<String>> getValues() { return values; }
public void setValues(List<List<String>> values) { this.values = values; }
}
}
}
3. LogQL Query Service
LogQL Query Builder and Executor
package com.example.loki.service;
import com.example.loki.client.LokiClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Service
public class LogQLService {
private static final Logger logger = LoggerFactory.getLogger(LogQLService.class);
private final LokiClient lokiClient;
public LogQLService(LokiClient lokiClient) {
this.lokiClient = lokiClient;
}
// Basic log query
public LogQueryResult queryLogs(String query, Instant start, Instant end,
Integer limit, String direction) {
try {
LokiClient.LokiQueryResponse response = lokiClient.queryRange(
query,
start.toEpochMilli() * 1000000, // Convert to nanoseconds
end.toEpochMilli() * 1000000,
limit != null ? limit.longValue() : null,
direction
);
return parseQueryResponse(response);
} catch (Exception e) {
logger.error("Error executing LogQL query: {}", query, e);
return new LogQueryResult(Collections.emptyList(), 0, e.getMessage());
}
}
// Query with relative time range
public LogQueryResult queryLogs(String query, String timeRange, Integer limit) {
TimeRange range = parseTimeRange(timeRange);
return queryLogs(query, range.start(), range.end(), limit, "backward");
}
// Parse query response into structured result
private LogQueryResult parseQueryResponse(LokiClient.LokiQueryResponse response) {
if (response == null || response.getData() == null) {
return new LogQueryResult(Collections.emptyList(), 0, "No data received");
}
List<LogEntry> entries = new ArrayList<>();
int totalLogs = 0;
for (LokiClient.LokiQueryResponse.StreamResult stream : response.getData().getResult()) {
Map<String, String> labels = stream.getStream();
List<List<String>> values = stream.getValues();
totalLogs += values.size();
for (List<String> value : values) {
if (value.size() >= 2) {
String timestamp = value.get(0);
String logLine = value.get(1);
LogEntry entry = new LogEntry(
parseNanosecondsTimestamp(timestamp),
logLine,
new HashMap<>(labels)
);
entries.add(entry);
}
}
}
// Sort by timestamp (newest first)
entries.sort((a, b) -> b.timestamp().compareTo(a.timestamp()));
return new LogQueryResult(entries, totalLogs, null);
}
// Metric queries
public MetricQueryResult queryMetrics(String query, Instant start, Instant end, String step) {
try {
LokiClient.LokiQueryResponse response = lokiClient.queryRange(
query,
start.toEpochMilli() * 1000000,
end.toEpochMilli() * 1000000,
null,
null
);
return parseMetricResponse(response);
} catch (Exception e) {
logger.error("Error executing metric query: {}", query, e);
return new MetricQueryResult(Collections.emptyList(), e.getMessage());
}
}
private MetricQueryResult parseMetricResponse(LokiClient.LokiQueryResponse response) {
if (response == null || response.getData() == null) {
return new MetricQueryResult(Collections.emptyList(), "No data received");
}
List<MetricSeries> series = new ArrayList<>();
for (LokiClient.LokiQueryResponse.StreamResult stream : response.getData().getResult()) {
Map<String, String> labels = stream.getStream();
List<List<String>> values = stream.getValues();
List<MetricPoint> points = values.stream()
.map(value -> {
if (value.size() >= 2) {
try {
double metricValue = Double.parseDouble(value.get(1));
return new MetricPoint(
parseNanosecondsTimestamp(value.get(0)),
metricValue
);
} catch (NumberFormatException e) {
logger.warn("Invalid metric value: {}", value.get(1));
return null;
}
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (!points.isEmpty()) {
series.add(new MetricSeries(labels, points));
}
}
return new MetricQueryResult(series, null);
}
// Label operations
public List<String> getAvailableLabels(String timeRange) {
TimeRange range = parseTimeRange(timeRange);
return lokiClient.labels(
String.valueOf(range.start().toEpochMilli() * 1000000),
String.valueOf(range.end().toEpochMilli() * 1000000)
);
}
public List<String> getLabelValues(String label, String timeRange) {
TimeRange range = parseTimeRange(timeRange);
return lokiClient.labelValues(
label,
String.valueOf(range.start().toEpochMilli() * 1000000),
String.valueOf(range.end().toEpochMilli() * 1000000)
);
}
// Query templates for common use cases
public LogQueryResult searchErrors(String application, String timeRange) {
String query = String.format(
"{application=\"%s\"} |= \"ERROR\" |= \"Exception\"",
application
);
return queryLogs(query, timeRange, 1000);
}
public LogQueryResult searchByTraceId(String traceId, String timeRange) {
String query = String.format(
"{} |= \"%s\"",
traceId
);
return queryLogs(query, timeRange, 100);
}
public MetricQueryResult getRequestRate(String application, String timeRange) {
String query = String.format(
"rate({application=\"%s\"} |= \"GET\" |= \"POST\" [5m])",
application
);
return queryMetrics(query, parseTimeRange(timeRange).start(),
parseTimeRange(timeRange).end(), "1m");
}
public MetricQueryResult getErrorRate(String application, String timeRange) {
String query = String.format(
"rate({application=\"%s\"} |= \"ERROR\" [5m])",
application
);
return queryMetrics(query, parseTimeRange(timeRange).start(),
parseTimeRange(timeRange).end(), "1m");
}
public MetricQueryResult getLogVolume(String application, String timeRange) {
String query = String.format(
"sum by (level) (count_over_time({application=\"%s\"}[5m]))",
application
);
return queryMetrics(query, parseTimeRange(timeRange).start(),
parseTimeRange(timeRange).end(), "5m");
}
// Utility methods
private LocalDateTime parseNanosecondsTimestamp(String nanosTimestamp) {
try {
long nanos = Long.parseLong(nanosTimestamp);
long millis = nanos / 1000000;
return LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault());
} catch (NumberFormatException e) {
logger.warn("Invalid timestamp format: {}", nanosTimestamp);
return LocalDateTime.now();
}
}
private TimeRange parseTimeRange(String timeRange) {
Instant end = Instant.now();
Instant start;
switch (timeRange.toLowerCase()) {
case "1h":
start = end.minusSeconds(3600);
break;
case "6h":
start = end.minusSeconds(21600);
break;
case "24h":
start = end.minusSeconds(86400);
break;
case "7d":
start = end.minusSeconds(604800);
break;
case "30d":
start = end.minusSeconds(2592000);
break;
default:
// Try to parse custom range like "2h", "30m"
Pattern pattern = Pattern.compile("^(\\d+)([mhd])$");
Matcher matcher = pattern.matcher(timeRange.toLowerCase());
if (matcher.matches()) {
int value = Integer.parseInt(matcher.group(1));
String unit = matcher.group(2);
switch (unit) {
case "m":
start = end.minusSeconds(value * 60L);
break;
case "h":
start = end.minusSeconds(value * 3600L);
break;
case "d":
start = end.minusSeconds(value * 86400L);
break;
default:
start = end.minusSeconds(3600); // Default 1 hour
}
} else {
start = end.minusSeconds(3600); // Default 1 hour
}
}
return new TimeRange(start, end);
}
// Data classes
public record LogQueryResult(List<LogEntry> entries, int totalCount, String error) {}
public record LogEntry(LocalDateTime timestamp, String message, Map<String, String> labels) {}
public record MetricQueryResult(List<MetricSeries> series, String error) {}
public record MetricSeries(Map<String, String> labels, List<MetricPoint> points) {}
public record MetricPoint(LocalDateTime timestamp, double value) {}
public record TimeRange(Instant start, Instant end) {}
}
4. Log Processing and Analysis
Log Analysis Service
package com.example.loki.analysis;
import com.example.loki.service.LogQLService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Service
public class LogAnalysisService {
private static final Logger logger = LoggerFactory.getLogger(LogAnalysisService.class);
private final LogQLService logQLService;
public LogAnalysisService(LogQLService logQLService) {
this.logQLService = logQLService;
}
// Error analysis
public ErrorAnalysis analyzeErrors(String application, String timeRange) {
LogQLService.LogQueryResult result = logQLService.searchErrors(application, timeRange);
Map<String, Integer> errorTypes = new HashMap<>();
Map<String, Integer> errorSources = new HashMap<>();
List<ErrorEntry> recentErrors = new ArrayList<>();
for (LogQLService.LogEntry entry : result.entries()) {
// Extract error type from log message
String errorType = extractErrorType(entry.message());
errorTypes.merge(errorType, 1, Integer::sum);
// Extract source component
String source = entry.labels().getOrDefault("component", "unknown");
errorSources.merge(source, 1, Integer::sum);
// Collect recent errors
if (recentErrors.size() < 50) { // Limit to 50 recent errors
recentErrors.add(new ErrorEntry(
entry.timestamp(),
entry.message(),
source,
errorType
));
}
}
return new ErrorAnalysis(
result.totalCount(),
sortByValueDesc(errorTypes),
sortByValueDesc(errorSources),
recentErrors
);
}
// Performance analysis
public PerformanceAnalysis analyzePerformance(String application, String timeRange) {
// Get request rate
LogQLService.MetricQueryResult requestRate = logQLService.getRequestRate(application, timeRange);
// Get error rate
LogQLService.MetricQueryResult errorRate = logQLService.getErrorRate(application, timeRange);
// Get response time patterns (if available in logs)
LogQLService.LogQueryResult responseTimeLogs = logQLService.queryLogs(
String.format("{application=\"%s\"} |= \"response_time\"", application),
timeRange, 1000
);
List<Double> responseTimes = extractResponseTimes(responseTimeLogs);
return new PerformanceAnalysis(
calculateAverageMetric(requestRate),
calculateAverageMetric(errorRate),
calculatePercentiles(responseTimes),
responseTimes.size()
);
}
// Pattern detection
public List<LogPattern> detectPatterns(String application, String timeRange) {
LogQLService.LogQueryResult result = logQLService.queryLogs(
String.format("{application=\"%s\"}", application),
timeRange, 5000
);
Map<String, PatternFrequency> patternFrequencies = new HashMap<>();
for (LogQLService.LogEntry entry : entry.result().entries()) {
String normalizedMessage = normalizeLogMessage(entry.message());
patternFrequencies.merge(normalizedMessage,
new PatternFrequency(normalizedMessage, entry.message(), 1, entry.timestamp()),
(existing, newFreq) -> new PatternFrequency(
normalizedMessage,
entry.message(),
existing.count() + 1,
entry.timestamp().isAfter(existing.lastSeen()) ?
entry.timestamp() : existing.lastSeen()
));
}
return patternFrequencies.values().stream()
.filter(pf -> pf.count() > 1) // Only patterns that occur multiple times
.sorted((a, b) -> Integer.compare(b.count(), a.count()))
.map(pf -> new LogPattern(pf.pattern(), pf.sample(), pf.count(), pf.lastSeen()))
.collect(Collectors.toList());
}
// Correlation analysis
public CorrelationAnalysis correlateErrorsWithMetrics(String application, String timeRange) {
ErrorAnalysis errorAnalysis = analyzeErrors(application, timeRange);
PerformanceAnalysis performanceAnalysis = analyzePerformance(application, timeRange);
// Simple correlation: check if error spikes correlate with performance degradation
double errorRate = performanceAnalysis.averageErrorRate();
int totalErrors = errorAnalysis.totalErrors();
String correlation;
if (errorRate > 0.1 && totalErrors > 10) { // Thresholds
correlation = "STRONG: High error rate with significant error count";
} else if (errorRate > 0.05 && totalErrors > 5) {
correlation = "MODERATE: Moderate error rate with noticeable error count";
} else {
correlation = "LOW: Normal error levels";
}
return new CorrelationAnalysis(
correlation,
errorAnalysis.totalErrors(),
performanceAnalysis.averageErrorRate(),
performanceAnalysis.averageRequestRate(),
findPeakErrorTime(errorAnalysis)
);
}
// Utility methods
private String extractErrorType(String logMessage) {
if (logMessage.contains("NullPointerException")) return "NullPointerException";
if (logMessage.contains("TimeoutException")) return "TimeoutException";
if (logMessage.contains("SQLException")) return "SQLException";
if (logMessage.contains("Connection refused")) return "ConnectionError";
if (logMessage.contains("OutOfMemoryError")) return "OutOfMemoryError";
if (logMessage.contains("FileNotFoundException")) return "FileNotFound";
// Extract exception class name if present
Pattern exceptionPattern = Pattern.compile("([a-zA-Z0-9]+\\.)+([A-Z][a-zA-Z]*Exception)");
java.util.regex.Matcher matcher = exceptionPattern.matcher(logMessage);
if (matcher.find()) {
return matcher.group(2);
}
return "UnknownError";
}
private String normalizeLogMessage(String logMessage) {
// Remove variable data (numbers, IDs, etc.)
return logMessage
.replaceAll("\\d+", "N")
.replaceAll("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", "UUID")
.replaceAll("\\b[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\b", "IP")
.replaceAll("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b", "EMAIL")
.trim();
}
private List<Double> extractResponseTimes(LogQLService.LogQueryResult result) {
List<Double> responseTimes = new ArrayList<>();
Pattern responseTimePattern = Pattern.compile("response_time[=:](\\d+\\.?\\d*)");
for (LogQLService.LogEntry entry : result.entries()) {
java.util.regex.Matcher matcher = responseTimePattern.matcher(entry.message());
if (matcher.find()) {
try {
responseTimes.add(Double.parseDouble(matcher.group(1)));
} catch (NumberFormatException e) {
// Ignore invalid numbers
}
}
}
return responseTimes;
}
private double calculateAverageMetric(LogQLService.MetricQueryResult metricResult) {
if (metricResult.series().isEmpty()) return 0.0;
double sum = 0.0;
int count = 0;
for (LogQLService.MetricSeries series : metricResult.series()) {
for (LogQLService.MetricPoint point : series.points()) {
sum += point.value();
count++;
}
}
return count > 0 ? sum / count : 0.0;
}
private Map<String, Integer> sortByValueDesc(Map<String, Integer> map) {
return map.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));
}
private Percentiles calculatePercentiles(List<Double> values) {
if (values.isEmpty()) {
return new Percentiles(0, 0, 0, 0);
}
List<Double> sorted = new ArrayList<>(values);
Collections.sort(sorted);
int size = sorted.size();
double p50 = sorted.get((int) (size * 0.5));
double p90 = sorted.get((int) (size * 0.9));
double p95 = sorted.get((int) (size * 0.95));
double p99 = size > 1 ? sorted.get((int) (size * 0.99)) : p95;
return new Percentiles(p50, p90, p95, p99);
}
private LocalDateTime findPeakErrorTime(ErrorAnalysis errorAnalysis) {
return errorAnalysis.recentErrors().stream()
.map(ErrorEntry::timestamp)
.max(LocalDateTime::compareTo)
.orElse(LocalDateTime.now());
}
// Data classes
public record ErrorAnalysis(int totalErrors, Map<String, Integer> errorTypes,
Map<String, Integer> errorSources, List<ErrorEntry> recentErrors) {}
public record ErrorEntry(LocalDateTime timestamp, String message, String source, String type) {}
public record PerformanceAnalysis(double averageRequestRate, double averageErrorRate,
Percentiles responseTimePercentiles, int sampleSize) {}
public record Percentiles(double p50, double p90, double p95, double p99) {}
public record LogPattern(String pattern, String sample, int frequency, LocalDateTime lastSeen) {}
public record CorrelationAnalysis(String correlation, int totalErrors, double errorRate,
double requestRate, LocalDateTime peakErrorTime) {}
private record PatternFrequency(String pattern, String sample, int count, LocalDateTime lastSeen) {}
}
5. Alerting and Monitoring
Alerting Service
```java
package com.example.loki.alerting;
import com.example.loki.service.LogQLService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@Service
public class AlertingService {
private static final Logger logger = LoggerFactory.getLogger(AlertingService.class);
private final LogQLService logQLService;
private final List<Alert> activeAlerts;
private final List<AlertRule> alertRules;
public AlertingService(LogQLService logQLService) {
this.logQLService = logQLService;
this.activeAlerts = new CopyOnWriteArrayList<>();
this.alertRules = initializeDefaultRules();
}
// Check all alert rules periodically
@Scheduled(fixedRate = 60000) // Run every minute
public void checkAlerts() {
logger.debug("Checking alert rules...");
for (AlertRule rule : alertRules) {
if (rule.enabled()) {
checkAlertRule(rule);
}
}
// Clean up resolved alerts
cleanResolvedAlerts();
}
private void checkAlertRule(AlertRule rule) {
try {
Instant end = Instant.now();
Instant start = end.minus(rule.timeRangeMinutes(), ChronoUnit.MINUTES);
LogQLService.MetricQueryResult result = logQLService.queryMetrics(
rule.query(), start, end, "1m"
);
if (result.error() == null && !result.series().isEmpty()) {
for (LogQLService.MetricSeries series : result.s