Comprehensive LogQL Integration Guide for Java Applications
1. Loki Client Configuration and Setup
Maven Dependencies
<!-- pom.xml -->
<properties>
<loki4j.version>1.4.0</loki4j.version>
<okhttp.version>4.12.0</okhttp.version>
<micrometer.version>1.11.5</micrometer.version>
</properties>
<dependencies>
<!-- Loki Logback Appender -->
<dependency>
<groupId>com.github.loki4j</groupId>
<artifactId>loki-logback-appender</artifactId>
<version>${loki4j.version}</version>
</dependency>
<!-- HTTP Client for Loki API -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Metrics -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>${micrometer.version}</version>
</dependency>
</dependencies>
2. Loki Client Service
// LokiClientService.java
package com.example.loki.client;
import com.fasterxml.jackson.databind.JsonNode;
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.Service;
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Service
public class LokiClientService {
private static final Logger logger = LoggerFactory.getLogger(LokiClientService.class);
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
@Value("${loki.url:http://localhost:3100}")
private String lokiUrl;
@Value("${loki.timeout.seconds:30}")
private int timeoutSeconds;
@Value("${loki.batch.size:1000}")
private int batchSize;
public LokiClientService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(timeoutSeconds, TimeUnit.SECONDS)
.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
.writeTimeout(timeoutSeconds, TimeUnit.SECONDS)
.build();
}
public LogQueryResult query(LogQLQuery query) {
try {
String url = buildQueryUrl(query);
Request request = new Request.Builder()
.url(url)
.get()
.build();
logger.debug("Executing LogQL query: {}", url);
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new LokiException("Loki query failed with code: " + response.code());
}
String responseBody = response.body().string();
return parseQueryResponse(responseBody, query);
}
} catch (IOException e) {
logger.error("Failed to execute Loki query", e);
throw new LokiException("Loki query execution failed", e);
}
}
public LogQueryResult queryRange(LogQLQuery query) {
try {
String url = buildQueryRangeUrl(query);
Request request = new Request.Builder()
.url(url)
.get()
.build();
logger.debug("Executing LogQL range query: {}", url);
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new LokiException("Loki range query failed with code: " + response.code());
}
String responseBody = response.body().string();
return parseQueryResponse(responseBody, query);
}
} catch (IOException e) {
logger.error("Failed to execute Loki range query", e);
throw new LokiException("Loki range query execution failed", e);
}
}
public List<LabelResult> getLabels(Instant start, Instant end) {
try {
String url = lokiUrl + "/loki/api/v1/labels?" +
"start=" + start.toEpochMilli() + "000000" +
"&end=" + end.toEpochMilli() + "000000";
Request request = new Request.Builder()
.url(url)
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new LokiException("Failed to fetch labels: " + response.code());
}
String responseBody = response.body().string();
return parseLabelsResponse(responseBody);
}
} catch (IOException e) {
logger.error("Failed to fetch Loki labels", e);
throw new LokiException("Failed to fetch Loki labels", e);
}
}
public List<LabelValueResult> getLabelValues(String labelName, Instant start, Instant end) {
try {
String url = lokiUrl + "/loki/api/v1/label/" + labelName + "/values?" +
"start=" + start.toEpochMilli() + "000000" +
"&end=" + end.toEpochMilli() + "000000";
Request request = new Request.Builder()
.url(url)
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new LokiException("Failed to fetch label values: " + response.code());
}
String responseBody = response.body().string();
return parseLabelValuesResponse(responseBody, labelName);
}
} catch (IOException e) {
logger.error("Failed to fetch Loki label values for: {}", labelName, e);
throw new LokiException("Failed to fetch Loki label values", e);
}
}
public void pushLogs(List<LogEntry> logEntries) {
if (logEntries.isEmpty()) {
return;
}
try {
String payload = buildPushPayload(logEntries);
RequestBody body = RequestBody.create(
payload, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(lokiUrl + "/loki/api/v1/push")
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new LokiException("Log push failed with code: " + response.code());
}
logger.debug("Successfully pushed {} log entries to Loki", logEntries.size());
}
} catch (IOException e) {
logger.error("Failed to push logs to Loki", e);
throw new LokiException("Failed to push logs to Loki", e);
}
}
private String buildQueryUrl(LogQLQuery query) {
StringBuilder url = new StringBuilder(lokiUrl + "/loki/api/v1/query?");
url.append("query=").append(query.getQuery());
if (query.getLimit() > 0) {
url.append("&limit=").append(query.getLimit());
}
if (query.getTime() != null) {
url.append("&time=").append(query.getTime().toEpochMilli()).append("000000");
} else {
url.append("&time=").append(Instant.now().toEpochMilli()).append("000000");
}
if (query.getDirection() != null) {
url.append("&direction=").append(query.getDirection());
}
return url.toString();
}
private String buildQueryRangeUrl(LogQLQuery query) {
StringBuilder url = new StringBuilder(lokiUrl + "/loki/api/v1/query_range?");
url.append("query=").append(query.getQuery());
if (query.getLimit() > 0) {
url.append("&limit=").append(query.getLimit());
}
url.append("&start=").append(query.getStart().toEpochMilli()).append("000000");
url.append("&end=").append(query.getEnd().toEpochMilli()).append("000000");
if (query.getStep() != null) {
url.append("&step=").append(query.getStep());
}
if (query.getDirection() != null) {
url.append("&direction=").append(query.getDirection());
}
return url.toString();
}
private LogQueryResult parseQueryResponse(String responseBody, LogQLQuery query) throws IOException {
JsonNode rootNode = objectMapper.readTree(responseBody);
LogQueryResult result = new LogQueryResult();
result.setQuery(query);
result.setStatus(rootNode.path("status").asText());
JsonNode dataNode = rootNode.path("data");
if (dataNode.has("result")) {
List<LogStream> streams = new ArrayList<>();
for (JsonNode streamNode : dataNode.path("result")) {
LogStream stream = parseLogStream(streamNode);
streams.add(stream);
}
result.setStreams(streams);
}
if (dataNode.has("resultType")) {
result.setResultType(dataNode.path("resultType").asText());
}
JsonNode statsNode = rootNode.path("stats");
if (!statsNode.isMissingNode()) {
result.setStats(parseStats(statsNode));
}
return result;
}
private LogStream parseLogStream(JsonNode streamNode) {
LogStream stream = new LogStream();
// Parse labels
JsonNode streamLabels = streamNode.path("stream");
Map<String, String> labels = new HashMap<>();
streamLabels.fields().forEachRemaining(entry -> {
labels.put(entry.getKey(), entry.getValue().asText());
});
stream.setLabels(labels);
// Parse values
List<LogEntry> entries = new ArrayList<>();
for (JsonNode valueNode : streamNode.path("values")) {
if (valueNode.isArray() && valueNode.size() == 2) {
String timestamp = valueNode.get(0).asText();
String logLine = valueNode.get(1).asText();
entries.add(new LogEntry(timestamp, logLine, labels));
}
}
stream.setEntries(entries);
return stream;
}
private QueryStats parseStats(JsonNode statsNode) {
QueryStats stats = new QueryStats();
JsonNode summaryNode = statsNode.path("summary");
if (!summaryNode.isMissingNode()) {
stats.setTotalBytesProcessed(summaryNode.path("bytesProcessedPerSecond").asLong());
stats.setTotalLinesProcessed(summaryNode.path("linesProcessedPerSecond").asLong());
stats.setTotalBytesProcessed(summaryNode.path("totalBytesProcessed").asLong());
stats.setTotalLinesProcessed(summaryNode.path("totalLinesProcessed").asLong());
}
JsonNode storeNode = statsNode.path("store");
if (!storeNode.isMissingNode()) {
stats.setChunksDownloaded(storeNode.path("chunksDownloaded").asInt());
stats.setChunksDownloadSize(storeNode.path("chunksDownloadSize").asLong());
stats.setChunksTotal(storeNode.path("chunksTotal").asInt());
}
return stats;
}
private List<LabelResult> parseLabelsResponse(String responseBody) throws IOException {
JsonNode rootNode = objectMapper.readTree(responseBody);
List<LabelResult> labels = new ArrayList<>();
for (JsonNode labelNode : rootNode.path("data")) {
labels.add(new LabelResult(labelNode.asText()));
}
return labels;
}
private List<LabelValueResult> parseLabelValuesResponse(String responseBody, String labelName) throws IOException {
JsonNode rootNode = objectMapper.readTree(responseBody);
List<LabelValueResult> values = new ArrayList<>();
for (JsonNode valueNode : rootNode.path("data")) {
values.add(new LabelValueResult(labelName, valueNode.asText()));
}
return values;
}
private String buildPushPayload(List<LogEntry> logEntries) throws IOException {
Map<String, Object> payload = new HashMap<>();
List<Map<String, Object>> streams = new ArrayList<>();
// Group entries by labels
Map<String, List<LogEntry>> entriesByLabels = new HashMap<>();
for (LogEntry entry : logEntries) {
String labelsKey = entry.getLabels().toString();
entriesByLabels.computeIfAbsent(labelsKey, k -> new ArrayList<>()).add(entry);
}
// Create streams
for (Map.Entry<String, List<LogEntry>> entry : entriesByLabels.entrySet()) {
Map<String, Object> stream = new HashMap<>();
stream.put("stream", entry.getValue().get(0).getLabels());
List<List<String>> values = new ArrayList<>();
for (LogEntry logEntry : entry.getValue()) {
values.add(Arrays.asList(logEntry.getTimestamp(), logEntry.getLogLine()));
}
stream.put("values", values);
streams.add(stream);
}
payload.put("streams", streams);
return objectMapper.writeValueAsString(payload);
}
public static class LokiException extends RuntimeException {
public LokiException(String message) {
super(message);
}
public LokiException(String message, Throwable cause) {
super(message, cause);
}
}
}
3. LogQL Query Builder
// LogQLQueryBuilder.java
package com.example.loki.query;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
public class LogQLQueryBuilder {
private String logStreamSelector;
private List<String> filterExpressions = new ArrayList<>();
private List<String> pipelineStages = new ArrayList<>();
private Integer limit;
private Instant start;
private Instant end;
private Instant time;
private String direction;
private String step;
public static LogQLQueryBuilder create() {
return new LogQLQueryBuilder();
}
public LogQLQueryBuilder withLogStream(String selector) {
this.logStreamSelector = selector;
return this;
}
public LogQLQueryBuilder withApp(String appName) {
this.logStreamSelector = "{app=\"" + appName + "\"}";
return this;
}
public LogQLQueryBuilder withNamespace(String namespace) {
this.logStreamSelector = "{namespace=\"" + namespace + "\"}";
return this;
}
public LogQLQueryBuilder withPod(String podName) {
this.logStreamSelector = "{pod=\"" + podName + "\"}";
return this;
}
public LogQLQueryBuilder withLabel(String label, String value) {
if (logStreamSelector == null) {
logStreamSelector = "{" + label + "=\"" + value + "\"}";
} else {
logStreamSelector = logStreamSelector.replace("}", "," + label + "=\"" + value + "\"}");
}
return this;
}
public LogQLQueryBuilder contains(String text) {
filterExpressions.add("|= `" + text + "`");
return this;
}
public LogQLQueryBuilder doesNotContain(String text) {
filterExpressions.add("!= `" + text + "`");
return this;
}
public LogQLQueryBuilder containsRegex(String regex) {
filterExpressions.add("|~ `" + regex + "`");
return this;
}
public LogQLQueryBuilder doesNotMatchRegex(String regex) {
filterExpressions.add("!~ `" + regex + "`");
return this;
}
public LogQLQueryBuilder jsonParser() {
pipelineStages.add("| json");
return this;
}
public LogQLQueryBuilder logfmtParser() {
pipelineStages.add("| logfmt");
return this;
}
public LogQLQueryBuilder regexParser(String pattern) {
pipelineStages.add("| regexp `" + pattern + "`");
return this;
}
public LogQLQueryBuilder labelFilter(String label, String operator, String value) {
pipelineStages.add("| " + label + " " + operator + " `" + value + "`");
return this;
}
public LogQLQueryBuilder lineFormat(String format) {
pipelineStages.add("| line_format `" + format + "`");
return this;
}
public LogQLQueryBuilder labelFormat(Map<String, String> mappings) {
StringBuilder format = new StringBuilder("| label_format ");
mappings.forEach((key, value) -> {
format.append(key).append("=\"").append(value).append("\" ");
});
pipelineStages.add(format.toString().trim());
return this;
}
public LogQLQueryBuilder rate(int range) {
pipelineStages.add("| rate(" + range + "s)");
return this;
}
public LogQLQueryBuilder countOverTime(int range) {
pipelineStages.add("| count_over_time(" + range + "s)");
return this;
}
public LogQLQueryBuilder bytesRate(int range) {
pipelineStages.add("| bytes_rate(" + range + "s)");
return this;
}
public LogQLQueryBuilder bytesOverTime(int range) {
pipelineStages.add("| bytes_over_time(" + range + "s)");
return this;
}
public LogQLQueryBuilder sumBy(String... labels) {
pipelineStages.add("| sum by (" + String.join(", ", labels) + ")");
return this;
}
public LogQLQueryBuilder avgBy(String... labels) {
pipelineStages.add("| avg by (" + String.join(", ", labels) + ")");
return this;
}
public LogQLQueryBuilder maxBy(String... labels) {
pipelineStages.add("| max by (" + String.join(", ", labels) + ")");
return this;
}
public LogQLQueryBuilder minBy(String... labels) {
pipelineStages.add("| min by (" + String.join(", ", labels) + ")");
return this;
}
public LogQLQueryBuilder limit(int limit) {
this.limit = limit;
return this;
}
public LogQLQueryBuilder timeRange(Instant start, Instant end) {
this.start = start;
this.end = end;
return this;
}
public LogQLQueryBuilder lastMinutes(int minutes) {
this.end = Instant.now();
this.start = end.minus(minutes, ChronoUnit.MINUTES);
return this;
}
public LogQLQueryBuilder lastHours(int hours) {
this.end = Instant.now();
this.start = end.minus(hours, ChronoUnit.HOURS);
return this;
}
public LogQLQueryBuilder lastDays(int days) {
this.end = Instant.now();
this.start = end.minus(days, ChronoUnit.DAYS);
return this;
}
public LogQLQueryBuilder time(Instant time) {
this.time = time;
return this;
}
public LogQLQueryBuilder direction(String direction) {
this.direction = direction;
return this;
}
public LogQLQueryBuilder step(String step) {
this.step = step;
return this;
}
public LogQLQuery build() {
if (logStreamSelector == null) {
throw new IllegalStateException("Log stream selector is required");
}
StringBuilder query = new StringBuilder(logStreamSelector);
// Add filter expressions
for (String filter : filterExpressions) {
query.append(" ").append(filter);
}
// Add pipeline stages
for (String stage : pipelineStages) {
query.append(" ").append(stage);
}
LogQLQuery logQLQuery = new LogQLQuery();
logQLQuery.setQuery(query.toString());
if (limit != null) {
logQLQuery.setLimit(limit);
}
if (start != null && end != null) {
logQLQuery.setStart(start);
logQLQuery.setEnd(end);
}
if (time != null) {
logQLQuery.setTime(time);
}
if (direction != null) {
logQLQuery.setDirection(direction);
}
if (step != null) {
logQLQuery.setStep(step);
}
return logQLQuery;
}
// Predefined query templates
public static LogQLQuery errorLogs(String appName, int lastMinutes) {
return LogQLQueryBuilder.create()
.withApp(appName)
.contains("ERROR")
.lastMinutes(lastMinutes)
.limit(100)
.build();
}
public static LogQLQuery requestRate(String appName, int rangeSeconds) {
return LogQLQueryBuilder.create()
.withApp(appName)
.contains("HTTP")
.jsonParser()
.rate(rangeSeconds)
.sumBy("method", "status")
.lastHours(1)
.build();
}
public static LogQLQuery applicationMetrics(String appName) {
return LogQLQueryBuilder.create()
.withApp(appName)
.logfmtParser()
.lastMinutes(30)
.limit(500)
.build();
}
public static LogQLQuery slowQueries(String appName, int thresholdMs) {
return LogQLQueryBuilder.create()
.withApp(appName)
.contains("SQL")
.jsonParser()
.labelFilter("duration", ">", String.valueOf(thresholdMs))
.lastHours(24)
.limit(50)
.build();
}
}
4. Data Model Classes
// LogQL Data Models
package com.example.loki.model;
import java.time.Instant;
import java.util.*;
public class LogQLQuery {
private String query;
private int limit;
private Instant start;
private Instant end;
private Instant time;
private String direction;
private String step;
// Getters and setters
public String getQuery() { return query; }
public void setQuery(String query) { this.query = query; }
public int getLimit() { return limit; }
public void setLimit(int limit) { this.limit = limit; }
public Instant getStart() { return start; }
public void setStart(Instant start) { this.start = start; }
public Instant getEnd() { return end; }
public void setEnd(Instant end) { this.end = end; }
public Instant getTime() { return time; }
public void setTime(Instant time) { this.time = time; }
public String getDirection() { return direction; }
public void setDirection(String direction) { this.direction = direction; }
public String getStep() { return step; }
public void setStep(String step) { this.step = step; }
}
package com.example.loki.model;
import java.util.List;
import java.util.Map;
public class LogQueryResult {
private LogQLQuery query;
private String status;
private String resultType;
private List<LogStream> streams;
private QueryStats stats;
// Getters and setters
public LogQLQuery getQuery() { return query; }
public void setQuery(LogQLQuery query) { this.query = query; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getResultType() { return status; }
public void setResultType(String resultType) { this.resultType = resultType; }
public List<LogStream> getStreams() { return streams; }
public void setStreams(List<LogStream> streams) { this.streams = streams; }
public QueryStats getStats() { return stats; }
public void setStats(QueryStats stats) { this.stats = stats; }
public int getTotalEntries() {
return streams.stream()
.mapToInt(stream -> stream.getEntries().size())
.sum();
}
public List<LogEntry> getAllEntries() {
return streams.stream()
.flatMap(stream -> stream.getEntries().stream())
.toList();
}
}
package com.example.loki.model;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LogStream {
private Map<String, String> labels = new HashMap<>();
private List<LogEntry> entries = new ArrayList<>();
// Getters and setters
public Map<String, String> getLabels() { return labels; }
public void setLabels(Map<String, String> labels) { this.labels = labels; }
public List<LogEntry> getEntries() { return entries; }
public void setEntries(List<LogEntry> entries) { this.entries = entries; }
public String getLabelValue(String labelName) {
return labels.get(labelName);
}
}
package com.example.loki.model;
import java.util.Map;
public class LogEntry {
private String timestamp;
private String logLine;
private Map<String, String> labels;
public LogEntry() {}
public LogEntry(String timestamp, String logLine, Map<String, String> labels) {
this.timestamp = timestamp;
this.logLine = logLine;
this.labels = labels;
}
// Getters and setters
public String getTimestamp() { return timestamp; }
public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
public String getLogLine() { return logLine; }
public void setLogLine(String logLine) { this.logLine = logLine; }
public Map<String, String> getLabels() { return labels; }
public void setLabels(Map<String, String> labels) { this.labels = labels; }
public long getTimestampAsLong() {
try {
return Long.parseLong(timestamp);
} catch (NumberFormatException e) {
return 0L;
}
}
}
package com.example.loki.model;
public class QueryStats {
private long totalBytesProcessed;
private long totalLinesProcessed;
private int chunksDownloaded;
private long chunksDownloadSize;
private int chunksTotal;
// Getters and setters
public long getTotalBytesProcessed() { return totalBytesProcessed; }
public void setTotalBytesProcessed(long totalBytesProcessed) { this.totalBytesProcessed = totalBytesProcessed; }
public long getTotalLinesProcessed() { return totalLinesProcessed; }
public void setTotalLinesProcessed(long totalLinesProcessed) { this.totalLinesProcessed = totalLinesProcessed; }
public int getChunksDownloaded() { return chunksDownloaded; }
public void setChunksDownloaded(int chunksDownloaded) { this.chunksDownloaded = chunksDownloaded; }
public long getChunksDownloadSize() { return chunksDownloadSize; }
public void setChunksDownloadSize(long chunksDownloadSize) { this.chunksDownloadSize = chunksDownloadSize; }
public int getChunksTotal() { return chunksTotal; }
public void setChunksTotal(int chunksTotal) { this.chunksTotal = chunksTotal; }
}
package com.example.loki.model;
public class LabelResult {
private String name;
public LabelResult(String name) {
this.name = name;
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
package com.example.loki.model;
public class LabelValueResult {
private String labelName;
private String value;
public LabelValueResult(String labelName, String value) {
this.labelName = labelName;
this.value = value;
}
// Getters and setters
public String getLabelName() { return labelName; }
public void setLabelName(String labelName) { this.labelName = labelName; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
5. Log Analysis Service
// LogAnalysisService.java
package com.example.loki.analysis;
import com.example.loki.client.LokiClientService;
import com.example.loki.model.*;
import com.example.loki.query.LogQLQueryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
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 LokiClientService lokiClient;
public LogAnalysisService(LokiClientService lokiClient) {
this.lokiClient = lokiClient;
}
public ErrorAnalysis analyzeErrors(String appName, int lastHours) {
LogQLQuery query = LogQLQueryBuilder.errorLogs(appName, lastHours * 60);
LogQueryResult result = lokiClient.queryRange(query);
ErrorAnalysis analysis = new ErrorAnalysis();
analysis.setAppName(appName);
analysis.setTimeRange(lastHours + "h");
analysis.setTotalErrors(result.getTotalEntries());
// Categorize errors
Map<String, Integer> errorCategories = new HashMap<>();
Map<String, Integer> errorMessages = new HashMap<>();
for (LogEntry entry : result.getAllEntries()) {
String logLine = entry.getLogLine();
// Categorize by error type
String category = categorizeError(logLine);
errorCategories.merge(category, 1, Integer::sum);
// Extract error message pattern
String messagePattern = extractErrorMessage(logLine);
errorMessages.merge(messagePattern, 1, Integer::sum);
}
analysis.setErrorCategories(errorCategories);
analysis.setErrorMessages(errorMessages);
return analysis;
}
public PerformanceAnalysis analyzePerformance(String appName, int lastHours) {
// Query for request logs
LogQLQuery requestQuery = LogQLQueryBuilder.create()
.withApp(appName)
.contains("HTTP")
.jsonParser()
.lastHours(lastHours)
.build();
LogQueryResult requestResult = lokiClient.queryRange(requestQuery);
PerformanceAnalysis analysis = new PerformanceAnalysis();
analysis.setAppName(appName);
analysis.setTimeRange(lastHours + "h");
List<RequestMetric> requestMetrics = new ArrayList<>();
Map<String, List<Long>> responseTimesByEndpoint = new HashMap<>();
Pattern jsonPattern = Pattern.compile("\"method\":\"(\\w+)\".*\"path\":\"([^\"]+)\".*\"status\":(\\d+).*\"duration\":(\\d+)");
for (LogEntry entry : result.getAllEntries()) {
String logLine = entry.getLogLine();
// Parse JSON log line
var matcher = jsonPattern.matcher(logLine);
if (matcher.find()) {
String method = matcher.group(1);
String path = matcher.group(2);
int status = Integer.parseInt(matcher.group(3));
long duration = Long.parseLong(matcher.group(4));
String endpoint = method + " " + path;
responseTimesByEndpoint
.computeIfAbsent(endpoint, k -> new ArrayList<>())
.add(duration);
requestMetrics.add(new RequestMetric(endpoint, status, duration, entry.getTimestampAsLong()));
}
}
analysis.setRequestMetrics(requestMetrics);
// Calculate statistics
Map<String, EndpointStats> endpointStats = new HashMap<>();
for (Map.Entry<String, List<Long>> entry : responseTimesByEndpoint.entrySet()) {
String endpoint = entry.getKey();
List<Long> durations = entry.getValue();
EndpointStats stats = new EndpointStats();
stats.setEndpoint(endpoint);
stats.setRequestCount(durations.size());
stats.setAverageResponseTime(durations.stream().mapToLong(Long::longValue).average().orElse(0.0));
stats.setP95ResponseTime(calculatePercentile(durations, 95));
stats.setP99ResponseTime(calculatePercentile(durations, 99));
stats.setErrorRate(calculateErrorRate(requestMetrics, endpoint));
endpointStats.put(endpoint, stats);
}
analysis.setEndpointStats(endpointStats);
return analysis;
}
public TrafficAnalysis analyzeTraffic(String appName, int lastHours) {
LogQLQuery trafficQuery = LogQLQueryBuilder.requestRate(appName, 60)
.lastHours(lastHours)
.build();
LogQueryResult result = lokiClient.queryRange(trafficQuery);
TrafficAnalysis analysis = new TrafficAnalysis();
analysis.setAppName(appName);
analysis.setTimeRange(lastHours + "h");
// Parse metric results
Map<String, List<DataPoint>> trafficData = new HashMap<>();
for (LogStream stream : result.getStreams()) {
String labels = stream.getLabels().toString();
List<DataPoint> dataPoints = stream.getEntries().stream()
.map(entry -> {
String[] parts = entry.getLogLine().split(" ");
double value = Double.parseDouble(parts[parts.length - 1]);
return new DataPoint(entry.getTimestampAsLong(), value);
})
.collect(Collectors.toList());
trafficData.put(labels, dataPoints);
}
analysis.setTrafficData(trafficData);
return analysis;
}
public SecurityAnalysis analyzeSecurity(String appName, int lastDays) {
LogQLQuery securityQuery = LogQLQueryBuilder.create()
.withApp(appName)
.containsRegex("(unauthorized|forbidden|authentication|authorization|security|attack)")
.lastDays(lastDays)
.limit(1000)
.build();
LogQueryResult result = lokiClient.queryRange(securityQuery);
SecurityAnalysis analysis = new SecurityAnalysis();
analysis.setAppName(appName);
analysis.setTimeRange(lastDays + "d");
analysis.setSecurityEvents(result.getTotalEntries());
Map<String, Integer> eventTypes = new HashMap<>();
Map<String, Integer> sourceIPs = new HashMap<>();
Pattern ipPattern = Pattern.compile("\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b");
for (LogEntry entry : result.getAllEntries()) {
String logLine = entry.getLogLine().toLowerCase();
// Categorize security events
if (logLine.contains("unauthorized")) {
eventTypes.merge("Unauthorized Access", 1, Integer::sum);
} else if (logLine.contains("forbidden")) {
eventTypes.merge("Forbidden Access", 1, Integer::sum);
} else if (logLine.contains("authentication")) {
eventTypes.merge("Authentication Issue", 1, Integer::sum);
} else if (logLine.contains("attack")) {
eventTypes.merge("Potential Attack", 1, Integer::sum);
} else {
eventTypes.merge("Other Security Event", 1, Integer::sum);
}
// Extract source IPs
var ipMatcher = ipPattern.matcher(logLine);
if (ipMatcher.find()) {
sourceIPs.merge(ipMatcher.group(), 1, Integer::sum);
}
}
analysis.setEventTypes(eventTypes);
analysis.setSourceIPs(sourceIPs);
return analysis;
}
public SystemHealthAnalysis analyzeSystemHealth(String appName, int lastHours) {
Instant end = Instant.now();
Instant start = end.minus(lastHours, ChronoUnit.HOURS);
SystemHealthAnalysis analysis = new SystemHealthAnalysis();
analysis.setAppName(appName);
analysis.setTimeRange(lastHours + "h");
// Get available labels
List<LabelResult> labels = lokiClient.getLabels(start, end);
analysis.setAvailableLabels(labels.stream().map(LabelResult::getName).collect(Collectors.toList()));
// Analyze log volume
LogQLQuery volumeQuery = LogQLQueryBuilder.create()
.withApp(appName)
.countOverTime(300) // 5-minute intervals
.lastHours(lastHours)
.build();
LogQueryResult volumeResult = lokiClient.queryRange(volumeQuery);
analysis.setLogVolume(volumeResult.getTotalEntries());
// Check for critical errors
LogQLQuery criticalQuery = LogQLQueryBuilder.create()
.withApp(appName)
.containsRegex("(CRITICAL|FATAL|panic|OutOfMemory)")
.lastHours(lastHours)
.build();
LogQueryResult criticalResult = lokiClient.queryRange(criticalQuery);
analysis.setCriticalErrors(criticalResult.getTotalEntries());
return analysis;
}
private String categorizeError(String logLine) {
logLine = logLine.toLowerCase();
if (logLine.contains("timeout") || logLine.contains("time out")) {
return "Timeout";
} else if (logLine.contains("connection") && logLine.contains("refused")) {
return "Connection Refused";
} else if (logLine.contains("null pointer")) {
return "Null Pointer Exception";
} else if (logLine.contains("out of memory")) {
return "Out of Memory";
} else if (logLine.contains("database") || logLine.contains("sql")) {
return "Database Error";
} else if (logLine.contains("network") || logLine.contains("socket")) {
return "Network Error";
} else if (logLine.contains("authentication") || logLine.contains("authorization")) {
return "Authentication Error";
} else if (logLine.contains("validation")) {
return "Validation Error";
} else {
return "Other Error";
}
}
private String extractErrorMessage(String logLine) {
// Extract the main error message, removing timestamps and other metadata
String[] parts = logLine.split("\\s+", 5);
if (parts.length > 4) {
return parts[4];
}
return logLine;
}
private double calculatePercentile(List<Long> values, double percentile) {
if (values.isEmpty()) return 0.0;
Collections.sort(values);
int index = (int) Math.ceil(percentile / 100.0 * values.size());
return values.get(Math.min(index, values.size() - 1));
}
private double calculateErrorRate(List<RequestMetric> metrics, String endpoint) {
long totalRequests = metrics.stream()
.filter(m -> m.getEndpoint().equals(endpoint))
.count();
long errorRequests = metrics.stream()
.filter(m -> m.getEndpoint().equals(endpoint))
.filter(m -> m.getStatus() >= 400)
.count();
return totalRequests > 0 ? (double) errorRequests / totalRequests : 0.0;
}
// Analysis result classes
public static class ErrorAnalysis {
private String appName;
private String timeRange;
private int totalErrors;
private Map<String, Integer> errorCategories;
private Map<String, Integer> errorMessages;
// Getters and setters
public String getAppName() { return appName; }
public void setAppName(String appName) { this.appName = appName; }
public String getTimeRange() { return timeRange; }
public void setTimeRange(String timeRange) { this.timeRange = timeRange; }
public int getTotalErrors() { return totalErrors; }
public void setTotalErrors(int totalErrors) { this.totalErrors = totalErrors; }
public Map<String, Integer> getErrorCategories() { return errorCategories; }
public void setErrorCategories(Map<String, Integer> errorCategories) { this.errorCategories = errorCategories; }
public Map<String, Integer> getErrorMessages() { return errorMessages; }
public void setErrorMessages(Map<String, Integer> errorMessages) { this.errorMessages = errorMessages; }
}
public static class PerformanceAnalysis {
private String appName;
private String timeRange;
private List<RequestMetric> requestMetrics;
private Map<String, EndpointStats> endpointStats;
// Getters and setters
public String getAppName() { return appName; }
public void setAppName(String appName) { this.appName = appName; }
public String getTimeRange() { return timeRange; }
public void setTimeRange(String timeRange) { this.timeRange = timeRange; }
public List<RequestMetric> getRequestMetrics() { return requestMetrics; }
public void setRequestMetrics(List<RequestMetric> requestMetrics) { this.requestMetrics = requestMetrics; }
public Map<String, EndpointStats> getEndpointStats() { return endpointStats; }
public void setEndpointStats(Map<String, EndpointStats> endpointStats) { this.endpointStats = endpointStats; }
}
public static class TrafficAnalysis {
private String appName;
private String timeRange;
private Map<String, List<DataPoint>> trafficData;
// Getters and setters
public String getAppName() { return appName; }
public void setAppName(String appName) { this.appName = appName; }
public String getTimeRange() { return timeRange; }
public void setTimeRange(String timeRange) { this.timeRange = timeRange; }
public Map<String, List<DataPoint>> getTrafficData() { return trafficData; }
public void setTrafficData(Map<String, List<DataPoint>> trafficData) { this.trafficData = trafficData; }
}
public static class SecurityAnalysis {
private String appName;
private String timeRange;
private int securityEvents;
private Map<String, Integer> eventTypes;
private Map<String, Integer> sourceIPs;
// Getters and setters
public String getAppName() { return appName; }
public void setAppName(String appName) { this.appName = appName; }
public String getTimeRange() { return timeRange; }
public void setTimeRange(String timeRange) { this.timeRange = timeRange; }
public int getSecurityEvents() { return securityEvents; }
public void setSecurityEvents(int securityEvents) { this.securityEvents = securityEvents; }
public Map<String, Integer> getEventTypes() { return eventTypes; }
public void setEventTypes(Map<String, Integer> eventTypes) { this.eventTypes = eventTypes; }
public Map<String, Integer> getSourceIPs() { return sourceIPs; }
public void setSourceIPs(Map<String, Integer> sourceIPs) { this.sourceIPs = sourceIPs; }
}
public static class SystemHealthAnalysis {
private String appName;
private String timeRange;
private List<String> availableLabels;
private int logVolume;
private int criticalErrors;
// Getters and setters
public String getAppName() { return appName; }
public void setAppName(String appName) { this.appName = appName; }
public String getTimeRange() { return timeRange; }
public void setTimeRange(String timeRange) { this.timeRange = timeRange; }
public List<String> getAvailableLabels() { return availableLabels; }
public void setAvailableLabels(List<String> availableLabels) { this.availableLabels = availableLabels; }
public int getLogVolume() { return logVolume; }
public void setLogVolume(int logVolume) { this.logVolume = logVolume; }
public int getCriticalErrors() { return criticalErrors; }
public void setCriticalErrors(int criticalErrors) { this.criticalErrors = criticalErrors; }
}
public static class RequestMetric {
private String endpoint;
private int status;
private long duration;
private long timestamp;
public RequestMetric(String endpoint, int status, long duration, long timestamp) {
this.endpoint = endpoint;
this.status = status;
this.duration = duration;
this.timestamp = timestamp;
}
// Getters
public String getEndpoint() { return endpoint; }
public int getStatus() { return status; }
public long getDuration() { return duration; }
public long getTimestamp() { return timestamp; }
}
public static class EndpointStats {
private String endpoint;
private int requestCount;
private double averageResponseTime;
private double p95ResponseTime;
private double p99ResponseTime;
private double errorRate;
// Getters and setters
public String getEndpoint() { return endpoint; }
public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
public int getRequestCount() { return requestCount; }
public void setRequestCount(int requestCount) { this.requestCount = requestCount; }
public double getAverageResponseTime() { return averageResponseTime; }
public void setAverageResponseTime(double averageResponseTime) { this.averageResponseTime = averageResponseTime; }
public double getP95ResponseTime() { return p95ResponseTime; }
public void setP95ResponseTime(double p95ResponseTime) { this.p95ResponseTime = p95ResponseTime; }
public double getP99ResponseTime() { return p99ResponseTime; }
public void setP99ResponseTime(double p99ResponseTime) { this.p99ResponseTime = p99ResponseTime; }
public double getErrorRate() { return errorRate; }
public void setErrorRate(double errorRate) { this.errorRate = errorRate; }
}
public static class DataPoint {
private long timestamp;
private double value;
public DataPoint(long timestamp, double value) {
this.timestamp = timestamp;
this.value = value;
}
// Getters
public long getTimestamp() { return timestamp; }
public double getValue() { return value; }
}
}
6. Logback Configuration for Loki
<!-- src/main/resources/logback-spring.xml -->
<configuration>
<springProperty scope="context" name="appName" source="spring.application.name"/>
<springProperty scope="context" name="lokiUrl" source="loki.url" defaultValue="http://localhost:3100"/>
<appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
<http>
<url>${lokiUrl}/loki/api/v1/push</url>
</http>
<format>
<label>
<pattern>app=${appName},host=${HOSTNAME},level=%level</pattern>
</label>
<message>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level %logger{36} - %msg%n</pattern>
</message>
<sortByTime>true</sortByTime>
</format>
</appender>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="LOKI" />
</root>
<!-- Application-specific logging -->
<logger name="com.example" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE" />
<appender-ref ref="LOKI" />
</logger>
</configuration>
7. Spring Boot Configuration
// LokiConfiguration.java
package com.example.loki.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "loki")
public class LokiConfiguration {
private String url = "http://localhost:3100";
private int timeoutSeconds = 30;
private int batchSize = 1000;
private boolean enabled = true;
// Getters and setters
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public int getTimeoutSeconds() { return timeoutSeconds; }
public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; }
public int getBatchSize() { return batchSize; }
public void setBatchSize(int batchSize) { this.batchSize = batchSize; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
}
# application.yml loki: url: http://localhost:3100 timeout-seconds: 30 batch-size: 1000 enabled: true logging: config: classpath:logback-spring.xml level: com.example.loki: DEBUG
This comprehensive Grafana Loki integration provides:
- Complete LogQL client implementation
- Query builder for programmatic query construction
- Advanced log analysis capabilities
- Real-time log ingestion via Logback appender
- Performance monitoring and analysis
- Security event detection
- System health monitoring
- Spring Boot auto-configuration
The setup enables powerful log analysis and monitoring capabilities using Grafana Loki's LogQL with the convenience of Java integration and Spring Boot compatibility.