This is an automated email from the ASF dual-hosted git repository. zhaoqingran pushed a commit to branch self-monitor in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
commit 5850edb6718278266356c07bc9f85936ab1281d9 Author: Logic <zqr10...@dromara.org> AuthorDate: Mon Jul 14 00:32:52 2025 +0800 feat(otel): add Prometheus metrics support - Implement MetricsService for collecting and exporting internal metrics - Add PrometheusCollector and Meter beans in OpenTelemetryConfig - Create PrometheusMetricsController to expose metrics via /metrics endpoint - Update pom.xml to include Prometheus exporter dependency --- hertzbeat-otel/pom.xml | 9 +++ .../hertzbeat/otel/config/OpenTelemetryConfig.java | 41 ++++++++++ .../controller/PrometheusMetricsController.java | 58 ++++++++++++++ .../hertzbeat/otel/service/MetricsService.java | 90 ++++++++++++++++++++++ 4 files changed, 198 insertions(+) diff --git a/hertzbeat-otel/pom.xml b/hertzbeat-otel/pom.xml index 43d1004bf2..bcf36c4bd4 100644 --- a/hertzbeat-otel/pom.xml +++ b/hertzbeat-otel/pom.xml @@ -32,6 +32,10 @@ </properties> <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> <!-- hertzbeat common --> <dependency> <groupId>org.apache.hertzbeat</groupId> @@ -51,6 +55,11 @@ <groupId>io.opentelemetry.instrumentation</groupId> <artifactId>opentelemetry-spring-boot-starter</artifactId> </dependency> + <dependency> + <groupId>io.opentelemetry</groupId> + <artifactId>opentelemetry-exporter-prometheus</artifactId> + <version>1.52.0-alpha</version> + </dependency> </dependencies> </project> diff --git a/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/config/OpenTelemetryConfig.java b/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/config/OpenTelemetryConfig.java index 44774dbd3a..6ec278f5f2 100644 --- a/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/config/OpenTelemetryConfig.java +++ b/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/config/OpenTelemetryConfig.java @@ -21,11 +21,13 @@ package org.apache.hertzbeat.otel.config; import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; import static org.apache.http.HttpHeaders.CONTENT_TYPE; +import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.resources.Resource; import java.util.Base64; @@ -159,4 +161,43 @@ public class OpenTelemetryConfig { return sdkLoggerProviderBuilder.addLogRecordProcessor(batchLogProcessor); }); } + + /** + * Configures the SdkMeterProvider for Prometheus scraping. + * This bean is conditionally created if the otel.exporter.prometheus.enabled property is true. + * + * @return SdkMeterProvider + */ + @Bean + @ConditionalOnProperty(name = "otel.exporter.prometheus.enabled", havingValue = "true") + public SdkMeterProvider sdkMeterProvider() { + return SdkMeterProvider.builder() + .setResource(Resource.create(io.opentelemetry.api.common.Attributes.of(SERVICE_NAME, HERTZBEAT_SERVICE_NAME))) + .build(); + } + + /** + * Creates a PrometheusCollector bean that registers with the SdkMeterProvider. + * This collector is then used by the PrometheusMetricsController to scrape metrics. + * + * @param sdkMeterProvider the SdkMeterProvider + * @return a PrometheusCollector + */ + @Bean + @ConditionalOnProperty(name = "otel.exporter.prometheus.enabled", havingValue = "true") + public PrometheusCollector prometheusCollector(SdkMeterProvider sdkMeterProvider) { + return PrometheusCollector.builder().setSdkMeterProvider(sdkMeterProvider).build(); + } + + /** + * Provides a Meter bean for dependency injection across the application (e.g., in MetricsService). + * + * @param sdkMeterProvider The SdkMeterProvider bean. + * @return Meter + */ + @Bean + @ConditionalOnProperty(name = "otel.exporter.prometheus.enabled", havingValue = "true") + public Meter meter(SdkMeterProvider sdkMeterProvider) { + return sdkMeterProvider.get(HERTZBEAT_SERVICE_NAME); + } } \ No newline at end of file diff --git a/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/controller/PrometheusMetricsController.java b/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/controller/PrometheusMetricsController.java new file mode 100644 index 0000000000..0c751b8601 --- /dev/null +++ b/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/controller/PrometheusMetricsController.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.otel.controller; + +import io.opentelemetry.exporter.prometheus.PrometheusCollector; +import io.prometheus.client.exporter.common.TextFormat; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.io.Writer; + +/** + * A Spring MVC controller to expose Prometheus metrics. + * This controller is enabled when `otel.exporter.prometheus.enabled` is set to `true`. + * It provides a `/metrics` endpoint that scrapes the OpenTelemetry metrics + * and returns them in the Prometheus text format. + */ +@RestController +@ConditionalOnProperty(name = "otel.exporter.prometheus.enabled", havingValue = "true") +public class PrometheusMetricsController { + + private final PrometheusCollector prometheusCollector; + + public PrometheusMetricsController(PrometheusCollector prometheusCollector) { + this.prometheusCollector = prometheusCollector; + } + + /** + * Handles GET requests to the /metrics endpoint. + * + * @param response the HttpServletResponse to write the metrics to + * @throws IOException if an I/O error occurs + */ + @GetMapping(value = "/metrics", produces = TextFormat.CONTENT_TYPE_004) + public void metrics(HttpServletResponse response) throws IOException { + try (Writer writer = response.getWriter()) { + TextFormat.write004(writer, prometheusCollector.collect()); + } + } +} \ No newline at end of file diff --git a/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/service/MetricsService.java b/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/service/MetricsService.java new file mode 100644 index 0000000000..7becbcc5b8 --- /dev/null +++ b/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/service/MetricsService.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.otel.service; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +/** + * Service for collecting and exporting HertzBeat's internal metrics using OpenTelemetry. + */ +@Service +public class MetricsService { + + private static final String STATUS_SUCCESS = "success"; + private static final String STATUS_FAIL = "fail"; + private static final String LABEL_STATUS = "status"; + private static final String LABEL_MONITOR_TYPE = "monitor_type"; + private static final String LABEL_MONITOR_ID = "monitor_id"; + private static final String LABEL_MONITOR_NAME = "monitor_name"; + private static final String LABEL_MONITOR_TARGET = "monitor_target"; + + private final LongCounter collectTotalCounter; + private final DoubleHistogram collectDurationHistogram; + + public MetricsService(Meter meter) { + this.collectTotalCounter = meter.counterBuilder("hertzbeat_collect_total") + .setDescription("The total number of collection tasks executed.") + .build(); + + this.collectDurationHistogram = meter.histogramBuilder("hertzbeat_collect_duration_seconds") + .setDescription("The duration of collection task executions, in seconds.") + .setUnit("s") + .build(); + } + + /** + * Records a successful collection task. + * + * @param monitor The monitor instance. + * @param duration The duration of the collection in milliseconds. + */ + public void recordCollect(Monitor monitor, long duration) { + Attributes attributes = buildAttributes(monitor, STATUS_SUCCESS); + collectTotalCounter.add(1, attributes); + collectDurationHistogram.record(TimeUnit.MILLISECONDS.toSeconds(duration), attributes); + } + + /** + * Records a failed collection task. + * + * @param monitor The monitor instance. + * @param duration The duration of the collection in milliseconds. + */ + public void recordCollect(Monitor monitor, long duration, Throwable throwable) { + Attributes attributes = buildAttributes(monitor, STATUS_FAIL); + collectTotalCounter.add(1, attributes); + collectDurationHistogram.record(TimeUnit.MILLISECONDS.toSeconds(duration), attributes); + } + + private Attributes buildAttributes(Monitor monitor, String status) { + return Attributes.builder() + .put(LABEL_STATUS, status) + .put(LABEL_MONITOR_ID, String.valueOf(monitor.getId())) + .put(LABEL_MONITOR_NAME, monitor.getName()) + .put(LABEL_MONITOR_TYPE, monitor.getApp()) + .put(LABEL_MONITOR_TARGET, monitor.getHost()) + .build(); + } +} \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@hertzbeat.apache.org For additional commands, e-mail: notifications-h...@hertzbeat.apache.org