VictoriaMetrics is a fast, cost-effective, and scalable time series database and monitoring solution. This comprehensive guide covers integrating VictoriaMetrics with Java applications for metrics collection, querying, and monitoring.
Architecture Overview
Java Application → VictoriaMetrics Client → VictoriaMetrics API → Metrics Collection → Push/Pull Model → Query Execution → PromQL/MetricsQL → Alert Management → Alerting Rules → Service Discovery → Kubernetes/Static
Prerequisites and Setup
Maven Dependencies
<properties>
<micrometer.version>1.11.5</micrometer.version>
<okhttp.version>4.11.0</okhttp.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- Metrics -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>${micrometer.version}</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>${micrometer.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>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>
Configuration Properties
# VictoriaMetrics Configuration
victoriametrics.url=http://localhost:8428
victoriametrics.read-timeout=30000
victoriametrics.connect-timeout=10000
victoriametrics.write-url=${victoriametrics.url}/api/v1/import/prometheus
victoriametrics.query-url=${victoriametrics.url}/api/v1/query
victoriametrics.query-range-url=${victoriametrics.url}/api/v1/query_range
victoriametrics.series-url=${victoriametrics.url}/api/v1/series
# Metrics Configuration
metrics.application.name=my-application
metrics.environment=production
metrics.enabled=true
metrics.push-interval=30s
# Authentication (if required)
victoriametrics.username=
victoriametrics.password=
victoriametrics.bearer-token=
Core VictoriaMetrics Client
1. HTTP Client Configuration
package com.yourapp.victoriametrics.client;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
@Component
public class VictoriaMetricsHttpClient {
private static final Logger logger = LoggerFactory.getLogger(VictoriaMetricsHttpClient.class);
private final OkHttpClient httpClient;
private final String baseUrl;
private final String writeUrl;
private final String queryUrl;
private final String queryRangeUrl;
private final String seriesUrl;
public VictoriaMetricsHttpClient(
@Value("${victoriametrics.url:http://localhost:8428}") String baseUrl,
@Value("${victoriametrics.read-timeout:30000}") int readTimeout,
@Value("${victoriametrics.connect-timeout:10000}") int connectTimeout,
@Value("${victoriametrics.username:}") String username,
@Value("${victoriametrics.password:}") String password,
@Value("${victoriametrics.bearer-token:}") String bearerToken) {
this.baseUrl = baseUrl;
this.writeUrl = baseUrl + "/api/v1/import/prometheus";
this.queryUrl = baseUrl + "/api/v1/query";
this.queryRangeUrl = baseUrl + "/api/v1/query_range";
this.seriesUrl = baseUrl + "/api/v1/series";
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder()
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.addInterceptor(new VictoriaMetricsInterceptor(username, password, bearerToken));
// Configure SSL (accept all certificates for development)
configureSSL(clientBuilder);
this.httpClient = clientBuilder.build();
}
private void configureSSL(OkHttpClient.Builder clientBuilder) {
try {
final TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
};
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
clientBuilder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]);
clientBuilder.hostnameVerifier((hostname, session) -> true);
} catch (NoSuchAlgorithmException | KeyManagementException e) {
logger.warn("Failed to configure SSL, using default configuration", e);
}
}
public OkHttpClient getHttpClient() {
return httpClient;
}
public String getWriteUrl() {
return writeUrl;
}
public String getQueryUrl() {
return queryUrl;
}
public String getQueryRangeUrl() {
return queryRangeUrl;
}
public String getSeriesUrl() {
return seriesUrl;
}
public String getBaseUrl() {
return baseUrl;
}
/**
* Custom interceptor for authentication and headers
*/
private static class VictoriaMetricsInterceptor implements Interceptor {
private final String username;
private final String password;
private final String bearerToken;
public VictoriaMetricsInterceptor(String username, String password, String bearerToken) {
this.username = username;
this.password = password;
this.bearerToken = bearerToken;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request.Builder requestBuilder = originalRequest.newBuilder();
// Add authentication
if (bearerToken != null && !bearerToken.isEmpty()) {
requestBuilder.header("Authorization", "Bearer " + bearerToken);
} else if (username != null && !username.isEmpty() && password != null && !password.isEmpty()) {
String credentials = Credentials.basic(username, password);
requestBuilder.header("Authorization", credentials);
}
// Add common headers
requestBuilder.header("Content-Type", "application/json")
.header("Accept", "application/json");
return chain.proceed(requestBuilder.build());
}
}
}
2. VictoriaMetrics Client
package com.yourapp.victoriametrics.client;
import com.fasterxml.jackson.core.JsonProcessingException;
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.stereotype.Component;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@Component
public class VictoriaMetricsClient {
private static final Logger logger = LoggerFactory.getLogger(VictoriaMetricsClient.class);
private final VictoriaMetricsHttpClient httpClient;
private final ObjectMapper objectMapper;
public VictoriaMetricsClient(VictoriaMetricsHttpClient httpClient, ObjectMapper objectMapper) {
this.httpClient = httpClient;
this.objectMapper = objectMapper;
}
/**
* Write metrics in Prometheus exposition format
*/
public boolean writeMetrics(String metricsData) {
try {
RequestBody body = RequestBody.create(
metricsData,
MediaType.parse("text/plain; version=0.0.4")
);
Request request = new Request.Builder()
.url(httpClient.getWriteUrl())
.post(body)
.build();
try (Response response = httpClient.getHttpClient().newCall(request).execute()) {
if (response.isSuccessful()) {
logger.debug("Successfully wrote metrics to VictoriaMetrics");
return true;
} else {
logger.error("Failed to write metrics. Status: {}, Body: {}",
response.code(), response.body() != null ? response.body().string() : "empty");
return false;
}
}
} catch (IOException e) {
logger.error("Error writing metrics to VictoriaMetrics", e);
return false;
}
}
/**
* Write metrics asynchronously
*/
public CompletableFuture<Boolean> writeMetricsAsync(String metricsData) {
return CompletableFuture.supplyAsync(() -> writeMetrics(metricsData));
}
/**
* Execute instant query
*/
public QueryResponse executeQuery(String query, Long time) {
try {
HttpUrl.Builder urlBuilder = HttpUrl.parse(httpClient.getQueryUrl()).newBuilder()
.addQueryParameter("query", query);
if (time != null) {
urlBuilder.addQueryParameter("time", time.toString());
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.get()
.build();
try (Response response = httpClient.getHttpClient().newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, QueryResponse.class);
} else {
logger.error("Query failed. Status: {}, Body: {}",
response.code(), response.body() != null ? response.body().string() : "empty");
throw new VictoriaMetricsException("Query execution failed with status: " + response.code());
}
}
} catch (IOException e) {
logger.error("Error executing query", e);
throw new VictoriaMetricsException("Error executing query", e);
}
}
/**
* Execute range query
*/
public QueryResponse executeRangeQuery(String query, Long start, Long end, String step) {
try {
HttpUrl.Builder urlBuilder = HttpUrl.parse(httpClient.getQueryRangeUrl()).newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("start", String.valueOf(start))
.addQueryParameter("end", String.valueOf(end))
.addQueryParameter("step", step);
Request request = new Request.Builder()
.url(urlBuilder.build())
.get()
.build();
try (Response response = httpClient.getHttpClient().newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, QueryResponse.class);
} else {
logger.error("Range query failed. Status: {}", response.code());
throw new VictoriaMetricsException("Range query execution failed");
}
}
} catch (IOException e) {
logger.error("Error executing range query", e);
throw new VictoriaMetricsException("Error executing range query", e);
}
}
/**
* Get series metadata
*/
public SeriesResponse getSeries(String match, Long start, Long end) {
try {
HttpUrl.Builder urlBuilder = HttpUrl.parse(httpClient.getSeriesUrl()).newBuilder()
.addQueryParameter("match[]", match);
if (start != null) {
urlBuilder.addQueryParameter("start", start.toString());
}
if (end != null) {
urlBuilder.addQueryParameter("end", end.toString());
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.get()
.build();
try (Response response = httpClient.getHttpClient().newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, SeriesResponse.class);
} else {
logger.error("Series query failed. Status: {}", response.code());
throw new VictoriaMetricsException("Series query execution failed");
}
}
} catch (IOException e) {
logger.error("Error executing series query", e);
throw new VictoriaMetricsException("Error executing series query", e);
}
}
/**
* Delete series by selector
*/
public boolean deleteSeries(String match, Long start, Long end) {
try {
HttpUrl.Builder urlBuilder = HttpUrl.parse(httpClient.getSeriesUrl()).newBuilder()
.addQueryParameter("match[]", match);
if (start != null) {
urlBuilder.addQueryParameter("start", start.toString());
}
if (end != null) {
urlBuilder.addQueryParameter("end", end.toString());
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.delete()
.build();
try (Response response = httpClient.getHttpClient().newCall(request).execute()) {
return response.isSuccessful();
}
} catch (IOException e) {
logger.error("Error deleting series", e);
return false;
}
}
/**
* Check VictoriaMetrics health
*/
public boolean healthCheck() {
try {
Request request = new Request.Builder()
.url(httpClient.getBaseUrl() + "/health")
.get()
.build();
try (Response response = httpClient.getHttpClient().newCall(request).execute()) {
return response.isSuccessful();
}
} catch (IOException e) {
logger.error("Health check failed", e);
return false;
}
}
/**
* Get VictoriaMetrics build info
*/
public BuildInfo getBuildInfo() {
try {
Request request = new Request.Builder()
.url(httpClient.getBaseUrl() + "/api/v1/status/buildinfo")
.get()
.build();
try (Response response = httpClient.getHttpClient().newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, BuildInfo.class);
} else {
throw new VictoriaMetricsException("Failed to get build info");
}
}
} catch (IOException e) {
logger.error("Error getting build info", e);
throw new VictoriaMetricsException("Error getting build info", e);
}
}
public static class VictoriaMetricsException extends RuntimeException {
public VictoriaMetricsException(String message) {
super(message);
}
public VictoriaMetricsException(String message, Throwable cause) {
super(message, cause);
}
}
}
3. Response Models
package com.yourapp.victoriametrics.client;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
@JsonIgnoreProperties(ignoreUnknown = true)
public class QueryResponse {
private String status;
private Data data;
// Getters and Setters
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Data getData() { return data; }
public void setData(Data data) { this.data = data; }
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Data {
private String resultType;
private List<Result> result;
public String getResultType() { return resultType; }
public void setResultType(String resultType) { this.resultType = resultType; }
public List<Result> getResult() { return result; }
public void setResult(List<Result> result) { this.result = result; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Result {
private Map<String, String> metric;
private List<Object> value; // [timestamp, value]
private List<List<Object>> values; // for range queries
public Map<String, String> getMetric() { return metric; }
public void setMetric(Map<String, String> metric) { this.metric = metric; }
public List<Object> getValue() { return value; }
public void setValue(List<Object> value) { this.value = value; }
public List<List<Object>> getValues() { return values; }
public void setValues(List<List<Object>> values) { this.values = values; }
// Helper methods
public Double getValueAsDouble() {
if (value != null && value.size() > 1 && value.get(1) instanceof String) {
try {
return Double.parseDouble((String) value.get(1));
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
public Long getTimestamp() {
if (value != null && value.size() > 0 && value.get(0) instanceof String) {
try {
return Long.parseLong((String) value.get(0));
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public class SeriesResponse {
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; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public class BuildInfo {
private String status;
private BuildData data;
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public BuildData getData() { return data; }
public void setData(BuildData data) { this.data = data; }
@JsonIgnoreProperties(ignoreUnknown = true)
public static class BuildData {
private String version;
private long buildTime;
private String branch;
private String goVersion;
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public long getBuildTime() { return buildTime; }
public void setBuildTime(long buildTime) { this.buildTime = buildTime; }
public String getBranch() { return branch; }
public void setBranch(String branch) { this.branch = branch; }
public String getGoVersion() { return goVersion; }
public void setGoVersion(String goVersion) { this.goVersion = goVersion; }
}
}
Metrics Collection Service
4. Micrometer Integration
package com.yourapp.victoriametrics.service;
import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import io.prometheus.client.CollectorRegistry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class MetricsService {
private final PrometheusMeterRegistry meterRegistry;
private final ConcurrentHashMap<String, Meter> meters;
// Common tags
private final String applicationName;
private final String environment;
public MetricsService(
@Value("${metrics.application.name:unknown}") String applicationName,
@Value("${metrics.environment:unknown}") String environment) {
this.applicationName = applicationName;
this.environment = environment;
this.meters = new ConcurrentHashMap<>();
// Create Prometheus registry
this.meterRegistry = new PrometheusMeterRegistry(io.micrometer.prometheus.PrometheusConfig.DEFAULT);
// Configure common tags
meterRegistry.config().commonTags(
"application", applicationName,
"environment", environment
);
// Configure histogram percentiles
meterRegistry.config().meterFilter(
new MeterFilter() {
@Override
public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {
if (id.getType() == Meter.Type.TIMER || id.getType() == Meter.Type.DISTRIBUTION_SUMMARY) {
return DistributionStatisticConfig.builder()
.percentiles(0.5, 0.75, 0.95, 0.99)
.percentilePrecision(2)
.build()
.merge(config);
}
return config;
}
}
);
}
/**
* Get meter registry for Spring Boot integration
*/
public PrometheusMeterRegistry getMeterRegistry() {
return meterRegistry;
}
/**
* Get metrics in Prometheus format
*/
public String scrape() {
return meterRegistry.scrape();
}
/**
* Create and register a counter
*/
public Counter createCounter(String name, String description, String... tags) {
String key = "counter:" + name;
return (Counter) meters.computeIfAbsent(key, k ->
Counter.builder(name)
.description(description)
.tags(tags)
.register(meterRegistry)
);
}
/**
* Increment counter
*/
public void incrementCounter(String name, String... tags) {
Counter counter = createCounter(name, "Automatically created counter", tags);
counter.increment();
}
/**
* Increment counter by amount
*/
public void incrementCounter(String name, double amount, String... tags) {
Counter counter = createCounter(name, "Automatically created counter", tags);
counter.increment(amount);
}
/**
* Create and register a gauge
*/
public <T extends Number> Gauge createGauge(String name, T number, String description, String... tags) {
String key = "gauge:" + name;
return (Gauge) meters.computeIfAbsent(key, k ->
Gauge.builder(name, number, Number::doubleValue)
.description(description)
.tags(tags)
.register(meterRegistry)
);
}
/**
* Create gauge with atomic long
*/
public AtomicLong createGauge(String name, String description, String... tags) {
AtomicLong gaugeValue = new AtomicLong(0);
String key = "gauge:" + name;
meters.computeIfAbsent(key, k ->
Gauge.builder(name, gaugeValue, AtomicLong::get)
.description(description)
.tags(tags)
.register(meterRegistry)
);
return gaugeValue;
}
/**
* Create and register a timer
*/
public Timer createTimer(String name, String description, String... tags) {
String key = "timer:" + name;
return (Timer) meters.computeIfAbsent(key, k ->
Timer.builder(name)
.description(description)
.tags(tags)
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry)
);
}
/**
* Record timer execution
*/
public void recordTimer(String name, long time, TimeUnit unit, String... tags) {
Timer timer = createTimer(name, "Automatically created timer", tags);
timer.record(time, unit);
}
/**
* Time a runnable
*/
public void time(String name, Runnable runnable, String... tags) {
Timer timer = createTimer(name, "Automatically created timer", tags);
timer.record(runnable);
}
/**
* Create and register a distribution summary
*/
public DistributionSummary createSummary(String name, String description, String... tags) {
String key = "summary:" + name;
return (DistributionSummary) meters.computeIfAbsent(key, k ->
DistributionSummary.builder(name)
.description(description)
.tags(tags)
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry)
);
}
/**
* Record value in distribution summary
*/
public void recordSummary(String name, double amount, String... tags) {
DistributionSummary summary = createSummary(name, "Automatically created summary", tags);
summary.record(amount);
}
/**
* Create custom metric with tags
*/
public Meter createCustomMetric(String name, Meter.Type type, Iterable<Measurement> measurements, String... tags) {
String key = "custom:" + name;
return meters.computeIfAbsent(key, k ->
Meter.builder(name, type, measurements)
.tags(tags)
.register(meterRegistry)
);
}
/**
* Remove meter by name
*/
public void removeMeter(String name) {
Meter meter = meters.remove(name);
if (meter != null) {
meterRegistry.remove(meter);
}
}
/**
* Get all registered meter names
*/
public Iterable<String> getMeterNames() {
return meters.keySet();
}
/**
* Get meter by name
*/
public Meter getMeter(String name) {
return meters.get(name);
}
}
5. Metrics Push Service
package com.yourapp.victoriametrics.service;
import com.yourapp.victoriametrics.client.VictoriaMetricsClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@Service
public class MetricsPushService {
private static final Logger logger = LoggerFactory.getLogger(MetricsPushService.class);
private final VictoriaMetricsClient vmClient;
private final MetricsService metricsService;
private final ScheduledExecutorService scheduler;
private final AtomicBoolean running = new AtomicBoolean(false);
private final boolean enabled;
private final long pushIntervalMs;
public MetricsPushService(
VictoriaMetricsClient vmClient,
MetricsService metricsService,
@Value("${metrics.enabled:true}") boolean enabled,
@Value("${metrics.push-interval:30s}") String pushInterval) {
this.vmClient = vmClient;
this.metricsService = metricsService;
this.enabled = enabled;
this.pushIntervalMs = parseDuration(pushInterval);
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "victoriametrics-pusher");
t.setDaemon(true);
return t;
});
}
@PostConstruct
public void init() {
if (enabled) {
start();
}
}
@PreDestroy
public void shutdown() {
stop();
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
/**
* Start pushing metrics
*/
public void start() {
if (running.compareAndSet(false, true)) {
logger.info("Starting VictoriaMetrics push service with interval: {} ms", pushIntervalMs);
scheduler.scheduleAtFixedRate(this::pushMetrics, 0, pushIntervalMs, TimeUnit.MILLISECONDS);
}
}
/**
* Stop pushing metrics
*/
public void stop() {
if (running.compareAndSet(true, false)) {
logger.info("Stopping VictoriaMetrics push service");
}
}
/**
* Push metrics to VictoriaMetrics
*/
public void pushMetrics() {
if (!running.get()) {
return;
}
try {
String metricsData = metricsService.scrape();
boolean success = vmClient.writeMetrics(metricsData);
if (success) {
logger.debug("Successfully pushed metrics to VictoriaMetrics");
} else {
logger.warn("Failed to push metrics to VictoriaMetrics");
}
} catch (Exception e) {
logger.error("Error pushing metrics to VictoriaMetrics", e);
}
}
/**
* Push metrics immediately (synchronous)
*/
public boolean pushMetricsNow() {
try {
String metricsData = metricsService.scrape();
return vmClient.writeMetrics(metricsData);
} catch (Exception e) {
logger.error("Error pushing metrics immediately", e);
return false;
}
}
/**
* Check if push service is running
*/
public boolean isRunning() {
return running.get();
}
private long parseDuration(String duration) {
if (duration == null || duration.isEmpty()) {
return 30000; // Default 30 seconds
}
try {
if (duration.endsWith("ms")) {
return Long.parseLong(duration.substring(0, duration.length() - 2));
} else if (duration.endsWith("s")) {
return Long.parseLong(duration.substring(0, duration.length() - 1)) * 1000;
} else if (duration.endsWith("m")) {
return Long.parseLong(duration.substring(0, duration.length() - 1)) * 60 * 1000;
} else if (duration.endsWith("h")) {
return Long.parseLong(duration.substring(0, duration.length() - 1)) * 60 * 60 * 1000;
} else {
return Long.parseLong(duration);
}
} catch (NumberFormatException e) {
logger.warn("Invalid duration format: {}, using default 30s", duration);
return 30000;
}
}
}
Query Service
6. Advanced Query Service
package com.yourapp.victoriametrics.service;
import com.yourapp.victoriametrics.client.VictoriaMetricsClient;
import com.yourapp.victoriametrics.client.QueryResponse;
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.stream.Collectors;
@Service
public class QueryService {
private static final Logger logger = LoggerFactory.getLogger(QueryService.class);
private final VictoriaMetricsClient vmClient;
public QueryService(VictoriaMetricsClient vmClient) {
this.vmClient = vmClient;
}
/**
* Execute simple metric query
*/
public List<MetricData> queryMetric(String metricName, Map<String, String> labels, Long time) {
String query = buildQuery(metricName, labels);
QueryResponse response = vmClient.executeQuery(query, time);
return convertToMetricData(response);
}
/**
* Execute range query for metric
*/
public List<TimeSeriesData> queryMetricRange(String metricName, Map<String, String> labels,
Long start, Long end, String step) {
String query = buildQuery(metricName, labels);
QueryResponse response = vmClient.executeRangeQuery(query, start, end, step);
return convertToTimeSeriesData(response);
}
/**
* Get current value of a metric
*/
public Optional<Double> getCurrentValue(String metricName, Map<String, String> labels) {
List<MetricData> results = queryMetric(metricName, labels, null);
if (!results.isEmpty() && results.get(0).getValue() != null) {
return Optional.of(results.get(0).getValue());
}
return Optional.empty();
}
/**
* Get metric values over time
*/
public Map<String, List<DataPoint>> getMetricHistory(String metricName, Map<String, String> labels,
Duration duration, String step) {
long end = System.currentTimeMillis() / 1000;
long start = end - duration.getSeconds();
List<TimeSeriesData> seriesData = queryMetricRange(metricName, labels, start, end, step);
return seriesData.stream()
.collect(Collectors.toMap(
TimeSeriesData::getSeriesName,
TimeSeriesData::getDataPoints
));
}
/**
* Calculate rate of metric
*/
public List<MetricData> queryRate(String metricName, Map<String, String> labels, String range) {
String query = String.format("rate(%s%s[%s])", metricName, buildLabelFilter(labels), range);
QueryResponse response = vmClient.executeQuery(query, null);
return convertToMetricData(response);
}
/**
* Calculate increase of metric
*/
public List<MetricData> queryIncrease(String metricName, Map<String, String> labels, String range) {
String query = String.format("increase(%s%s[%s])", metricName, buildLabelFilter(labels), range);
QueryResponse response = vmClient.executeQuery(query, null);
return convertToMetricData(response);
}
/**
* Get top N metrics by value
*/
public List<MetricData> queryTopN(String metricName, int limit, boolean max) {
String direction = max ? "desc" : "asc";
String query = String.format("topk(%d, %s)", limit, metricName);
QueryResponse response = vmClient.executeQuery(query, null);
List<MetricData> results = convertToMetricData(response);
// Sort results
results.sort((a, b) -> {
int comparison = Double.compare(b.getValue(), a.getValue());
return max ? comparison : -comparison;
});
return results.stream().limit(limit).collect(Collectors.toList());
}
/**
* Execute custom PromQL query
*/
public QueryResponse executeCustomQuery(String promQL, Long time) {
return vmClient.executeQuery(promQL, time);
}
/**
* Execute custom range query
*/
public QueryResponse executeCustomRangeQuery(String promQL, Long start, Long end, String step) {
return vmClient.executeRangeQuery(promQL, start, end, step);
}
/**
* Get series metadata
*/
public List<Map<String, String>> getSeriesMetadata(String match, Long start, Long end) {
return vmClient.getSeries(match, start, end).getData();
}
/**
* Find metrics by label pattern
*/
public List<String> findMetricsByLabel(String labelName, String pattern) {
String match = String.format("{%s=~\"%s\"}", labelName, pattern);
List<Map<String, String>> series = getSeriesMetadata(match, null, null);
return series.stream()
.map(labels -> labels.get("__name__"))
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}
/**
* Build PromQL query from metric name and labels
*/
private String buildQuery(String metricName, Map<String, String> labels) {
return metricName + buildLabelFilter(labels);
}
/**
* Build label filter string
*/
private String buildLabelFilter(Map<String, String> labels) {
if (labels == null || labels.isEmpty()) {
return "";
}
String labelFilters = labels.entrySet().stream()
.map(entry -> String.format("%s=\"%s\"", entry.getKey(), entry.getValue()))
.collect(Collectors.joining(","));
return "{" + labelFilters + "}";
}
/**
* Convert QueryResponse to MetricData
*/
private List<MetricData> convertToMetricData(QueryResponse response) {
if (response == null || response.getData() == null || response.getData().getResult() == null) {
return Collections.emptyList();
}
return response.getData().getResult().stream()
.map(result -> {
MetricData data = new MetricData();
data.setMetricName(getMetricName(result.getMetric()));
data.setLabels(result.getMetric());
data.setValue(result.getValueAsDouble());
data.setTimestamp(result.getTimestamp());
return data;
})
.collect(Collectors.toList());
}
/**
* Convert QueryResponse to TimeSeriesData
*/
private List<TimeSeriesData> convertToTimeSeriesData(QueryResponse response) {
if (response == null || response.getData() == null || response.getData().getResult() == null) {
return Collections.emptyList();
}
return response.getData().getResult().stream()
.map(result -> {
TimeSeriesData series = new TimeSeriesData();
series.setSeriesName(getMetricName(result.getMetric()));
series.setLabels(result.getMetric());
if (result.getValues() != null) {
List<DataPoint> dataPoints = result.getValues().stream()
.map(value -> {
DataPoint point = new DataPoint();
point.setTimestamp(((Number) value.get(0)).longValue());
point.setValue(Double.parseDouble(value.get(1).toString()));
return point;
})
.collect(Collectors.toList());
series.setDataPoints(dataPoints);
}
return series;
})
.collect(Collectors.toList());
}
private String getMetricName(Map<String, String> labels) {
return labels != null ? labels.get("__name__") : "unknown";
}
// Data transfer objects
public static class MetricData {
private String metricName;
private Map<String, String> labels;
private Double value;
private Long timestamp;
// Getters and Setters
public String getMetricName() { return metricName; }
public void setMetricName(String metricName) { this.metricName = metricName; }
public Map<String, String> getLabels() { return labels; }
public void setLabels(Map<String, String> labels) { this.labels = labels; }
public Double getValue() { return value; }
public void setValue(Double value) { this.value = value; }
public Long getTimestamp() { return timestamp; }
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
public LocalDateTime getTimestampAsDateTime() {
if (timestamp != null) {
return LocalDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault());
}
return null;
}
}
public static class TimeSeriesData {
private String seriesName;
private Map<String, String> labels;
private List<DataPoint> dataPoints;
// Getters and Setters
public String getSeriesName() { return seriesName; }
public void setSeriesName(String seriesName) { this.seriesName = seriesName; }
public Map<String, String> getLabels() { return labels; }
public void setLabels(Map<String, String> labels) { this.labels = labels; }
public List<DataPoint> getDataPoints() { return dataPoints; }
public void setDataPoints(List<DataPoint> dataPoints) { this.dataPoints = dataPoints; }
}
public static class DataPoint {
private Long timestamp;
private Double value;
// Getters and Setters
public Long getTimestamp() { return timestamp; }
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
public Double getValue() { return value; }
public void setValue(Double value) { this.value = value; }
public LocalDateTime getTimestampAsDateTime() {
if (timestamp != null) {
return LocalDateTime.ofInstant(Instant.ofEpochSecond(timestamp), ZoneId.systemDefault());
}
return null;
}
public String getFormattedTimestamp() {
LocalDateTime dateTime = getTimestampAsDateTime();
if (dateTime != null) {
return dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
return null;
}
}
}
REST API Controllers
7. Metrics Controller
package com.yourapp.victoriametrics.controller;
import com.yourapp.victoriametrics.service.MetricsService;
import com.yourapp.victoriametrics.service.MetricsPushService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/metrics")
public class MetricsController {
private final MetricsService metricsService;
private final MetricsPushService pushService;
public MetricsController(MetricsService metricsService, MetricsPushService pushService) {
this.metricsService = metricsService;
this.pushService = pushService;
}
@GetMapping("/scrape")
public ResponseEntity<String> scrapeMetrics() {
String metrics = metricsService.scrape();
return ResponseEntity.ok()
.header("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
.body(metrics);
}
@PostMapping("/counter/{name}/increment")
public ResponseEntity<?> incrementCounter(
@PathVariable String name,
@RequestParam(defaultValue = "1") double amount,
@RequestParam Map<String, String> tags) {
String[] tagArray = tags.entrySet().stream()
.flatMap(entry -> java.util.stream.Stream.of(entry.getKey(), entry.getValue()))
.toArray(String[]::new);
metricsService.incrementCounter(name, amount, tagArray);
return ResponseEntity.ok().build();
}
@PostMapping("/gauge/{name}")
public ResponseEntity<?> setGauge(
@PathVariable String name,
@RequestParam double value,
@RequestParam Map<String, String> tags) {
String[] tagArray = tags.entrySet().stream()
.flatMap(entry -> java.util.stream.Stream.of(entry.getKey(), entry.getValue()))
.toArray(String[]::new);
metricsService.recordSummary(name, value, tagArray);
return ResponseEntity.ok().build();
}
@PostMapping("/timer/{name}/record")
public ResponseEntity<?> recordTimer(
@PathVariable String name,
@RequestParam long duration,
@RequestParam Map<String, String> tags) {
String[] tagArray = tags.entrySet().stream()
.flatMap(entry -> java.util.stream.Stream.of(entry.getKey(), entry.getValue()))
.toArray(String[]::new);
metricsService.recordTimer(name, duration, java.util.concurrent.TimeUnit.MILLISECONDS, tagArray);
return ResponseEntity.ok().build();
}
@PostMapping("/push")
public ResponseEntity<?> pushMetrics() {
boolean success = pushService.pushMetricsNow();
if (success) {
return ResponseEntity.ok().body(Map.of("status", "success", "message", "Metrics pushed successfully"));
} else {
return ResponseEntity.status(500).body(Map.of("status", "error", "message", "Failed to push metrics"));
}
}
@PostMapping("/push/start")
public ResponseEntity<?> startPushService() {
pushService.start();
return ResponseEntity.ok().body(Map.of("status", "started"));
}
@PostMapping("/push/stop")
public ResponseEntity<?> stopPushService() {
pushService.stop();
return ResponseEntity.ok().body(Map.of("status", "stopped"));
}
@GetMapping("/push/status")
public ResponseEntity<?> getPushStatus() {
return ResponseEntity.ok().body(Map.of("running", pushService.isRunning()));
}
}
8. Query Controller
package com.yourapp.victoriametrics.controller;
import com.yourapp.victoriametrics.client.QueryResponse;
import com.yourapp.victoriametrics.service.QueryService;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/query")
public class QueryController {
private final QueryService queryService;
public QueryController(QueryService queryService) {
this.queryService = queryService;
}
@GetMapping("/metric/{metricName}")
public ResponseEntity<?> queryMetric(
@PathVariable String metricName,
@RequestParam Map<String, String> labels,
@RequestParam(required = false) Long time) {
try {
List<QueryService.MetricData> results = queryService.queryMetric(metricName, labels, time);
return ResponseEntity.ok(results);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/metric/{metricName}/range")
public ResponseEntity<?> queryMetricRange(
@PathVariable String metricName,
@RequestParam Map<String, String> labels,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime end,
@RequestParam String step) {
try {
long startEpoch = start.atZone(ZoneId.systemDefault()).toEpochSecond();
long endEpoch = end.atZone(ZoneId.systemDefault()).toEpochSecond();
List<QueryService.TimeSeriesData> results = queryService.queryMetricRange(
metricName, labels, startEpoch, endEpoch, step);
return ResponseEntity.ok(results);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/metric/{metricName}/current")
public ResponseEntity<?> getCurrentValue(
@PathVariable String metricName,
@RequestParam Map<String, String> labels) {
try {
java.util.Optional<Double> value = queryService.getCurrentValue(metricName, labels);
if (value.isPresent()) {
return ResponseEntity.ok(Map.of("value", value.get()));
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/metric/{metricName}/rate")
public ResponseEntity<?> queryRate(
@PathVariable String metricName,
@RequestParam Map<String, String> labels,
@RequestParam String range) {
try {
List<QueryService.MetricData> results = queryService.queryRate(metricName, labels, range);
return ResponseEntity.ok(results);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/custom")
public ResponseEntity<?> executeCustomQuery(
@RequestParam String query,
@RequestParam(required = false) Long time) {
try {
QueryResponse response = queryService.executeCustomQuery(query, time);
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/series")
public ResponseEntity<?> getSeries(
@RequestParam String match,
@RequestParam(required = false) Long start,
@RequestParam(required = false) Long end) {
try {
List<Map<String, String>> series = queryService.getSeriesMetadata(match, start, end);
return ResponseEntity.ok(series);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/find-metrics")
public ResponseEntity<?> findMetricsByLabel(
@RequestParam String labelName,
@RequestParam String pattern) {
try {
List<String> metrics = queryService.findMetricsByLabel(labelName, pattern);
return ResponseEntity.ok(metrics);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
}
Configuration and Spring Boot Integration
9. Spring Boot Configuration
package com.yourapp.victoriametrics.config;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.prometheus.PrometheusConfig;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import io.prometheus.client.CollectorRegistry;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags(
@Value("${metrics.application.name:unknown}") String applicationName,
@Value("${metrics.environment:unknown}") String environment) {
return registry -> registry.config()
.commonTags(
"application", applicationName,
"environment", environment,
"instance", getHostName()
)
.meterFilter(MeterFilter.ignoreTags("too.much.information"))
.meterFilter(MeterFilter.denyNameStartsWith("jvm.threads"));
}
@Bean
public PrometheusMeterRegistry prometheusMeterRegistry(PrometheusConfig config,
CollectorRegistry collectorRegistry,
Clock clock) {
return new PrometheusMeterRegistry(config, collectorRegistry, clock);
}
private String getHostName() {
try {
return java.net.InetAddress.getLocalHost().getHostName();
} catch (java.net.UnknownHostException e) {
return "unknown";
}
}
}
This comprehensive VictoriaMetrics integration provides a complete solution for metrics collection, querying, and monitoring in Java applications. The system supports both push and pull models, integrates seamlessly with Micrometer, and provides extensive query capabilities through the VictoriaMetrics API.