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.