diff --git a/.github/workflows/backend-build-test.yml b/.github/workflows/backend-build-test.yml
index abb52e8960e..a71c0a0b19c 100644
--- a/.github/workflows/backend-build-test.yml
+++ b/.github/workflows/backend-build-test.yml
@@ -76,7 +76,7 @@ jobs:
- name: Build backend Maven E2E modules
run: |
mvnd clean -B package \
- -pl hertzbeat-e2e/hertzbeat-collector-common-e2e,hertzbeat-e2e/hertzbeat-collector-kafka-e2e,hertzbeat-e2e/hertzbeat-collector-basic-e2e,hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e,hertzbeat-e2e/hertzbeat-log-e2e \
+ -pl hertzbeat-e2e/hertzbeat-collector-common-e2e,hertzbeat-e2e/hertzbeat-collector-kafka-e2e,hertzbeat-e2e/hertzbeat-collector-basic-e2e,hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e,hertzbeat-e2e/hertzbeat-observability-e2e \
-am \
-Dmaven.test.skip=false \
--file pom.xml
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ConfigConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ConfigConstants.java
index 47e8bacfe33..e3b19977748 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ConfigConstants.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/ConfigConstants.java
@@ -69,6 +69,8 @@ interface FunctionModuleConstants {
String GRAFANA = "grafana";
String LOG = "log";
+
+ String OBSERVABILITY = "observability";
}
}
diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/NetworkConstants.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/NetworkConstants.java
index 7f1f3647855..8e1210d43aa 100644
--- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/NetworkConstants.java
+++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/constants/NetworkConstants.java
@@ -61,7 +61,7 @@ interface HttpClientConstants {
Duration READ_TIMEOUT = Duration.ofSeconds(6);
Duration WRITE_TIMEOUT = Duration.ofSeconds(6);
Duration CONNECT_TIMEOUT = Duration.ofSeconds(6);
- Duration GREPTIME_QUERY_READ_TIMEOUT = Duration.ofSeconds(5);
+ Duration GREPTIME_QUERY_READ_TIMEOUT = Duration.ofSeconds(15);
Duration GREPTIME_QUERY_CONNECT_TIMEOUT = Duration.ofSeconds(2);
Duration GREPTIME_WRITE_READ_TIMEOUT = Duration.ofSeconds(3);
Duration GREPTIME_WRITE_CONNECT_TIMEOUT = Duration.ofSeconds(2);
diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/pom.xml b/hertzbeat-e2e/hertzbeat-observability-e2e/pom.xml
similarity index 97%
rename from hertzbeat-e2e/hertzbeat-log-e2e/pom.xml
rename to hertzbeat-e2e/hertzbeat-observability-e2e/pom.xml
index a5638e23a20..dfffdb62c70 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/pom.xml
+++ b/hertzbeat-e2e/hertzbeat-observability-e2e/pom.xml
@@ -25,7 +25,7 @@
4.0.0
- hertzbeat-log-e2e
+ hertzbeat-observability-e2e${java.version}
@@ -52,7 +52,7 @@
org.apache.hertzbeat
- hertzbeat-log
+ hertzbeat-observability${hertzbeat.version}test
diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogPeriodicAlertE2eTest.java b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/alert/LogPeriodicAlertE2eTest.java
similarity index 99%
rename from hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogPeriodicAlertE2eTest.java
rename to hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/alert/LogPeriodicAlertE2eTest.java
index 9957ce417b0..cb5abb26f43 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogPeriodicAlertE2eTest.java
+++ b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/alert/LogPeriodicAlertE2eTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.alert;
+package org.apache.hertzbeat.observability.alert;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler;
diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogRealTimeAlertE2eTest.java b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/alert/LogRealTimeAlertE2eTest.java
similarity index 99%
rename from hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogRealTimeAlertE2eTest.java
rename to hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/alert/LogRealTimeAlertE2eTest.java
index 4ae973b343c..1e93a4c77a4 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/alert/LogRealTimeAlertE2eTest.java
+++ b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/alert/LogRealTimeAlertE2eTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.alert;
+package org.apache.hertzbeat.observability.alert;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/ingestion/LogIngestionE2eTest.java b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/ingestion/LogIngestionE2eTest.java
similarity index 98%
rename from hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/ingestion/LogIngestionE2eTest.java
rename to hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/ingestion/LogIngestionE2eTest.java
index 57feada98ed..5fcb3e0668c 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/ingestion/LogIngestionE2eTest.java
+++ b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/ingestion/LogIngestionE2eTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.ingestion;
+package org.apache.hertzbeat.observability.ingestion;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.log.LogEntry;
diff --git a/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/storage/GreptimeEntityFreeSignalStorageE2eTest.java b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/storage/GreptimeEntityFreeSignalStorageE2eTest.java
new file mode 100644
index 00000000000..c6ab50e64a6
--- /dev/null
+++ b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/storage/GreptimeEntityFreeSignalStorageE2eTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.observability.storage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.protobuf.ByteString;
+import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
+import io.opentelemetry.proto.common.v1.AnyValue;
+import io.opentelemetry.proto.common.v1.KeyValue;
+import io.opentelemetry.proto.logs.v1.LogRecord;
+import io.opentelemetry.proto.logs.v1.ResourceLogs;
+import io.opentelemetry.proto.logs.v1.ScopeLogs;
+import io.opentelemetry.proto.resource.v1.Resource;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeOtlpSignalStorage;
+import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.client.RestTemplate;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+/** Proves the warehouse-owned Entity-free OTLP log path against a real GreptimeDB. */
+@Testcontainers
+class GreptimeEntityFreeSignalStorageE2eTest {
+
+ private static final int GREPTIME_HTTP_PORT = 4000;
+ private static final int GREPTIME_GRPC_PORT = 4001;
+ private static final String LOG_SCHEMA = "greptime/tables/hertzbeat_logs.sql";
+ private static final String LOG_PIPELINE = "greptime/pipelines/hertzbeat_otlp_log_v1.yaml";
+ private static final String PIPELINE_NAME = "hertzbeat_otlp_log_v1";
+ private static final String TRACE_ID = "0123456789abcdef0123456789abcdef";
+ private static final String SPAN_ID = "0123456789abcdef";
+ private static final String BODY = "entity-free greptime proof";
+ private static final long LOG_TIME_NANOS = 1_710_000_000_123_456_789L;
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ @Container
+ @SuppressWarnings("resource")
+ private static final GenericContainer> GREPTIME = new GenericContainer<>(
+ DockerImageName.parse("greptime/greptimedb:latest"))
+ .withExposedPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT)
+ .withCommand("standalone", "start",
+ "--http-addr", "0.0.0.0:" + GREPTIME_HTTP_PORT,
+ "--rpc-bind-addr", "0.0.0.0:" + GREPTIME_GRPC_PORT)
+ .waitingFor(Wait.forListeningPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT))
+ .withStartupTimeout(Duration.ofSeconds(120));
+
+ private final HttpClient httpClient = HttpClient.newHttpClient();
+
+ @Test
+ void warehouseStorageShouldPersistEntityFreeOtlpLogs() throws Exception {
+ executeSql(classpathResource(LOG_SCHEMA).strip().replaceFirst(";\\s*$", ""));
+ uploadPipeline();
+
+ GreptimeOtlpSignalStorage storage = new GreptimeOtlpSignalStorage(
+ new GreptimeProperties(true, GREPTIME.getHost() + ':' + GREPTIME.getMappedPort(GREPTIME_GRPC_PORT),
+ endpoint(), "public", "", ""),
+ new RestTemplate());
+
+ storage.writeProtobuf("logs", request().toByteArray());
+
+ await().atMost(Duration.ofSeconds(30)).pollInterval(Duration.ofSeconds(1)).untilAsserted(() -> {
+ String sql = "SELECT COUNT(*) AS count FROM hertzbeat_logs WHERE trace_id = '" + TRACE_ID
+ + "' AND body = '" + BODY + "'";
+ assertThat(queryCount(sql)).isEqualTo(1);
+ });
+ }
+
+ private void uploadPipeline() throws Exception {
+ String boundary = "----hertzbeat-entity-free-proof";
+ String body = "--" + boundary + "\r\n"
+ + "Content-Disposition: form-data; name=\"file\"; filename=\"pipeline.yaml\"\r\n"
+ + "Content-Type: application/x-yaml\r\n\r\n"
+ + classpathResource(LOG_PIPELINE) + "\r\n"
+ + "--" + boundary + "--\r\n";
+ HttpResponse response = httpClient.send(HttpRequest.newBuilder()
+ .uri(URI.create(endpoint() + "/v1/pipelines/" + PIPELINE_NAME))
+ .header("Content-Type", "multipart/form-data; boundary=" + boundary)
+ .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
+ .build(), HttpResponse.BodyHandlers.ofString());
+ assertThat(response.statusCode()).as(response.body()).isBetween(200, 299);
+ }
+
+ private ExportLogsServiceRequest request() {
+ LogRecord record = LogRecord.newBuilder()
+ .setTimeUnixNano(LOG_TIME_NANOS)
+ .setObservedTimeUnixNano(LOG_TIME_NANOS)
+ .setSeverityNumberValue(9)
+ .setSeverityText("INFO")
+ .setBody(AnyValue.newBuilder().setStringValue(BODY).build())
+ .setTraceId(ByteString.copyFrom(hexToBytes(TRACE_ID)))
+ .setSpanId(ByteString.copyFrom(hexToBytes(SPAN_ID)))
+ .build();
+ return ExportLogsServiceRequest.newBuilder()
+ .addResourceLogs(ResourceLogs.newBuilder()
+ .setResource(Resource.newBuilder()
+ .addAttributes(stringAttribute("service.name", "checkout"))
+ .addAttributes(stringAttribute("deployment.environment.name", "test"))
+ .build())
+ .addScopeLogs(ScopeLogs.newBuilder().addLogRecords(record).build())
+ .build())
+ .build();
+ }
+
+ private int queryCount(String sql) throws Exception {
+ JsonNode rows = OBJECT_MAPPER.readTree(executeSql(sql).body())
+ .path("output").path(0).path("records").path("rows");
+ assertThat(rows.isArray()).isTrue();
+ assertThat(rows).isNotEmpty();
+ return rows.get(0).get(0).asInt();
+ }
+
+ private HttpResponse executeSql(String sql) throws Exception {
+ HttpResponse response = httpClient.send(HttpRequest.newBuilder()
+ .uri(URI.create(endpoint() + "/v1/sql?db=public"))
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .POST(HttpRequest.BodyPublishers.ofString(
+ "sql=" + URLEncoder.encode(sql, StandardCharsets.UTF_8), StandardCharsets.UTF_8))
+ .build(), HttpResponse.BodyHandlers.ofString());
+ assertThat(response.statusCode()).as(response.body()).isBetween(200, 299);
+ return response;
+ }
+
+ private String classpathResource(String path) throws Exception {
+ try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) {
+ assertThat(input).as(path).isNotNull();
+ return new String(input.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+
+ private static KeyValue stringAttribute(String key, String value) {
+ return KeyValue.newBuilder().setKey(key)
+ .setValue(AnyValue.newBuilder().setStringValue(value).build()).build();
+ }
+
+ private static byte[] hexToBytes(String value) {
+ byte[] bytes = new byte[value.length() / 2];
+ for (int index = 0; index < value.length(); index += 2) {
+ bytes[index / 2] = (byte) Integer.parseInt(value.substring(index, index + 2), 16);
+ }
+ return bytes;
+ }
+
+ private static String endpoint() {
+ return "http://" + GREPTIME.getHost() + ':' + GREPTIME.getMappedPort(GREPTIME_HTTP_PORT);
+ }
+}
diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/storage/GreptimeLogStorageE2eTest.java b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/storage/GreptimeLogStorageE2eTest.java
similarity index 99%
rename from hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/storage/GreptimeLogStorageE2eTest.java
rename to hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/storage/GreptimeLogStorageE2eTest.java
index d56a46ee6c6..7c62fb38462 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/java/org/apache/hertzbeat/log/storage/GreptimeLogStorageE2eTest.java
+++ b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/java/org/apache/hertzbeat/observability/storage/GreptimeLogStorageE2eTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.storage;
+package org.apache.hertzbeat.observability.storage;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.log.LogEntry;
diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/sureness.yml
similarity index 95%
rename from hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
rename to hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/sureness.yml
index dff4e65966f..c11eabc1568 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
+++ b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/sureness.yml
@@ -80,12 +80,12 @@ resourceRole:
- /api/ai/**===post===[admin]
- /api/ai/**===put===[admin]
- /api/ai/**===delete===[admin]
- - /api/logs/sse/**===get===[admin,user,guest]
+ - /api/otlp/v1/**===post===[admin,user]
+ # deprecated 1.8.x OTLP log aliases, forwarded to /api/otlp/v1/logs, removed in 2.0
+ - /api/logs/otlp/**===post===[admin,user]
- /api/logs/ingest/**===post===[admin,user]
- - /api/otlp/**===post===[admin,user]
- - /api/ingestion/otlp/**===get===[admin,user,guest]
- - /api/logs/**===get===[admin,user,guest]
- - /api/traces/**===get===[admin,user,guest]
+ - /api/observability/logs===delete===[admin]
+ - /api/observability/**===get===[admin,user,guest]
# The OpenAPI document is a map of every route, parameter and model, so it is
# scoped like any other administrative resource instead of being anonymous
- /v3/api-docs/**===get===[admin]
@@ -102,7 +102,6 @@ excludedResource:
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
- - /api/observability/capability===get
- /api/push/**===*
- /api/status/page/public/**===*
- /api/manager/sse/**===*
diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/vector.yml b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/vector.yml
similarity index 99%
rename from hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/vector.yml
rename to hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/vector.yml
index f86f2b01001..baea29f4c7d 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/vector.yml
+++ b/hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/vector.yml
@@ -102,7 +102,7 @@ sinks:
type: opentelemetry
protocol:
type: http
- uri: "http://host.testcontainers.internal:${HERTZBEAT_PORT:-1157}/api/logs/ingest/otlp"
+ uri: "http://host.testcontainers.internal:${HERTZBEAT_PORT:-1157}/api/otlp/v1/logs"
method: post
encoding:
codec: json
diff --git a/hertzbeat-e2e/pom.xml b/hertzbeat-e2e/pom.xml
index 6249cf0d447..2f261897528 100644
--- a/hertzbeat-e2e/pom.xml
+++ b/hertzbeat-e2e/pom.xml
@@ -32,7 +32,7 @@
hertzbeat-collector-kafka-e2ehertzbeat-collector-basic-e2ehertzbeat-collector-mysql-r2dbc-e2e
- hertzbeat-log-e2e
+ hertzbeat-observability-e2e
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogIngestionController.java b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogIngestionController.java
deleted file mode 100644
index e32510e4d68..00000000000
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogIngestionController.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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.log.controller;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import lombok.extern.slf4j.Slf4j;
-import java.util.List;
-
-import org.apache.hertzbeat.common.constants.CommonConstants;
-import org.apache.hertzbeat.common.entity.dto.Message;
-import org.apache.hertzbeat.log.service.LogProtocolAdapter;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-/**
- * Generic Log Ingestion Controller
- * Provides a fallback endpoint for log protocols that don't have dedicated controllers.
- * For OTLP protocol, use OtlpLogController instead.
- */
-@Tag(name = "Log Ingestion Controller")
-@RestController
-@RequestMapping(path = "/api/logs", produces = MediaType.APPLICATION_JSON_VALUE)
-@Slf4j
-public class LogIngestionController {
-
- private final List protocolAdapters;
-
- public LogIngestionController(List protocolAdapters) {
- this.protocolAdapters = protocolAdapters;
- }
-
- /**
- * Receive log payload pushed from external system specifying the log protocol.
- *
- * @param protocol log protocol identifier (e.g., "vector", "loki")
- * @param content raw request body
- */
- @Operation(summary = "Ingest logs by protocol name")
- @PostMapping(value = "/ingest/{protocol}", consumes = MediaType.APPLICATION_JSON_VALUE)
- public ResponseEntity> ingestLog(@PathVariable("protocol") String protocol,
- @RequestBody String content) {
- log.debug("Receive log from protocol: {}, content length: {}", protocol, content == null ? 0 : content.length());
-
- for (LogProtocolAdapter adapter : protocolAdapters) {
- if (adapter.supportProtocol().equalsIgnoreCase(protocol)) {
- try {
- adapter.ingest(content);
- return ResponseEntity.ok(Message.success("Add extern log success"));
- } catch (Exception e) {
- log.error("Add log failed: {}", e.getMessage(), e);
- return ResponseEntity.status(HttpStatus.BAD_REQUEST)
- .body(Message.fail(CommonConstants.FAIL_CODE, "Add extern log failed: " + e.getMessage()));
- }
- }
- }
- log.warn("Not support extern log from protocol: {}", protocol);
- return ResponseEntity.status(HttpStatus.BAD_REQUEST)
- .body(Message.fail(CommonConstants.FAIL_CODE, "Not support the " + protocol + " protocol log"));
- }
-}
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/OtlpLogController.java b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/OtlpLogController.java
deleted file mode 100644
index 3d010750810..00000000000
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/OtlpLogController.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * 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.log.controller;
-
-import com.fasterxml.jackson.core.io.JsonStringEncoder;
-import com.google.protobuf.InvalidProtocolBufferException;
-import com.google.protobuf.util.JsonFormat;
-import com.google.rpc.Status;
-import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.hertzbeat.log.service.impl.OtlpLogProtocolAdapter;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-/**
- * OTLP Log Ingestion Controller
- * Implements OTLP/HTTP specification for log ingestion.
- * Supports both binary-encoded Protobuf (application/x-protobuf) and JSON-encoded Protobuf (application/json).
- *
- * @see OTLP/HTTP Specification
- */
-@Tag(name = "OTLP Log Controller")
-@RestController
-@RequestMapping(path = "/api/logs/otlp")
-@Slf4j
-public class OtlpLogController {
-
- private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf";
-
- private static final ExportLogsServiceResponse EMPTY_RESPONSE = ExportLogsServiceResponse.newBuilder().build();
-
- private final OtlpLogProtocolAdapter otlpLogProtocolAdapter;
-
- public OtlpLogController(OtlpLogProtocolAdapter otlpLogProtocolAdapter) {
- this.otlpLogProtocolAdapter = otlpLogProtocolAdapter;
- }
-
- /**
- * OTLP/HTTP standard endpoint for logs with JSON-encoded Protobuf payload.
- * Content-Type: application/json
- *
- * Response follows OTLP specification:
- * - Success: HTTP 200 with ExportLogsServiceResponse (JSON encoded)
- * - Failure: HTTP 400 with google.rpc.Status (JSON encoded)
- *
- * @param content JSON-encoded ExportLogsServiceRequest
- * @return ExportLogsServiceResponse on success, Status on failure
- */
- @Operation(summary = "Ingest OTLP logs (JSON format)")
- @PostMapping(value = "/v1/logs", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
- public ResponseEntity ingestJsonLogs(@RequestBody String content) {
- log.debug("Receive OTLP JSON logs, content length: {}", content == null ? 0 : content.length());
- try {
- otlpLogProtocolAdapter.ingest(content);
- return ResponseEntity.ok(toJsonResponse(EMPTY_RESPONSE));
- } catch (IllegalArgumentException e) {
- return ResponseEntity.status(HttpStatus.BAD_REQUEST)
- .body(toJsonErrorResponse(e.getMessage()));
- } catch (Exception e) {
- // Server-side errors - unexpected failure
- log.error("Unexpected error ingesting OTLP JSON logs: {}", e.getMessage(), e);
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
- .body(toJsonErrorResponse(e.getMessage()));
- }
- }
-
- /**
- * OTLP/HTTP standard endpoint for logs with binary-encoded Protobuf payload.
- * Content-Type: application/x-protobuf
- *
- * Response follows OTLP specification:
- * - Success: HTTP 200 with ExportLogsServiceResponse (binary encoded)
- * - Failure: HTTP 400 with google.rpc.Status (binary encoded)
- *
- * @param content binary-encoded ExportLogsServiceRequest
- * @return ExportLogsServiceResponse on success, Status on failure
- */
- @Operation(summary = "Ingest OTLP logs (binary Protobuf format)")
- @PostMapping(value = "/v1/logs", consumes = CONTENT_TYPE_PROTOBUF, produces = CONTENT_TYPE_PROTOBUF)
- public ResponseEntity ingestBinaryLogs(@RequestBody byte[] content) {
- log.debug("Receive OTLP binary logs, content length: {}", content == null ? 0 : content.length);
- try {
- otlpLogProtocolAdapter.ingestBinary(content);
- return ResponseEntity.ok(EMPTY_RESPONSE.toByteArray());
- } catch (IllegalArgumentException e) {
- // Client-side validation errors - malformed request
- return ResponseEntity.status(HttpStatus.BAD_REQUEST)
- .body(createBinaryErrorResponse(e.getMessage()));
- } catch (Exception e) {
- // Server-side errors - unexpected failure
- log.error("Unexpected error ingesting OTLP binary logs: {}", e.getMessage(), e);
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
- .body(createBinaryErrorResponse(e.getMessage()));
- }
- }
-
- private String toJsonResponse(ExportLogsServiceResponse response) {
- try {
- return JsonFormat.printer().print(response);
- } catch (InvalidProtocolBufferException e) {
- log.error("Failed to convert ExportLogsServiceResponse to JSON: {}", e.getMessage(), e);
- return "{}";
- }
- }
-
- private String toJsonErrorResponse(String message) {
- Status status = Status.newBuilder()
- .setMessage(message != null ? message : "Unknown error")
- .build();
- try {
- return JsonFormat.printer().print(status);
- } catch (InvalidProtocolBufferException e) {
- return "{\"message\":\"" + escapeJson(message) + "\"}";
- }
- }
-
- /**
- * Escapes a string value for safe inclusion in JSON.
- *
- * @param message the string to escape
- * @return the escaped string, or empty string if message is null
- */
- private String escapeJson(String message) {
- if (message == null) {
- return "";
- }
-
- char[] escaped = JsonStringEncoder.getInstance().quoteAsString(message);
- return new String(escaped);
- }
-
- private byte[] createBinaryErrorResponse(String message) {
- return Status.newBuilder()
- .setMessage(message != null ? message : "Unknown error")
- .build()
- .toByteArray();
- }
-}
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogIngestionControllerTest.java b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogIngestionControllerTest.java
deleted file mode 100644
index cb6eb3678fc..00000000000
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogIngestionControllerTest.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * 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.log.controller;
-
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.when;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
-
-import java.util.Arrays;
-import java.util.List;
-
-import org.apache.hertzbeat.common.constants.CommonConstants;
-import org.apache.hertzbeat.log.service.LogProtocolAdapter;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.http.MediaType;
-import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
-import org.springframework.test.web.servlet.setup.MockMvcBuilders;
-
-/**
- * Unit test for {@link LogIngestionController}
- */
-@ExtendWith(MockitoExtension.class)
-class LogIngestionControllerTest {
-
- private MockMvc mockMvc;
-
- @Mock
- private LogProtocolAdapter vectorAdapter;
-
- private LogIngestionController logIngestionController;
-
- @BeforeEach
- void setUp() {
- List adapters = Arrays.asList(vectorAdapter);
- this.logIngestionController = new LogIngestionController(adapters);
- this.mockMvc = MockMvcBuilders.standaloneSetup(logIngestionController).build();
- }
-
- @Test
- void testIngestLogWithKnownProtocol() throws Exception {
- String logContent = "{\"message\":\"Test log message\"}";
-
- when(vectorAdapter.supportProtocol()).thenReturn("vector");
- doNothing().when(vectorAdapter).ingest(anyString());
-
- mockMvc.perform(
- MockMvcRequestBuilders
- .post("/api/logs/ingest/vector")
- .contentType(MediaType.APPLICATION_JSON)
- .content(logContent)
- )
- .andDo(print())
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
- .andExpect(jsonPath("$.msg").value("Add extern log success"))
- .andReturn();
- }
-
- @Test
- void testIngestLogWithUnsupportedProtocol() throws Exception {
- String logContent = "{\"message\":\"Unsupported protocol log\"}";
-
- when(vectorAdapter.supportProtocol()).thenReturn("vector");
-
- mockMvc.perform(
- MockMvcRequestBuilders
- .post("/api/logs/ingest/unsupported")
- .contentType(MediaType.APPLICATION_JSON)
- .content(logContent)
- )
- .andExpect(status().isBadRequest())
- .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE))
- .andExpect(jsonPath("$.msg").value("Not support the unsupported protocol log"));
- }
-
- @Test
- void testIngestLogWithAdapterException() throws Exception {
- String logContent = "{\"message\":\"Log message that will cause exception\"}";
-
- when(vectorAdapter.supportProtocol()).thenReturn("vector");
- doThrow(new IllegalArgumentException("Invalid log format")).when(vectorAdapter).ingest(anyString());
-
- mockMvc.perform(
- MockMvcRequestBuilders
- .post("/api/logs/ingest/vector")
- .contentType(MediaType.APPLICATION_JSON)
- .content(logContent)
- )
- .andExpect(status().isBadRequest())
- .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE))
- .andExpect(jsonPath("$.msg").value("Add extern log failed: Invalid log format"));
- }
-}
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/OtlpLogControllerTest.java b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/OtlpLogControllerTest.java
deleted file mode 100644
index d2e67bcb8ee..00000000000
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/OtlpLogControllerTest.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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.log.controller;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
-
-import org.apache.hertzbeat.log.service.impl.OtlpLogProtocolAdapter;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.http.MediaType;
-import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
-import org.springframework.test.web.servlet.setup.MockMvcBuilders;
-
-/**
- * Unit test for {@link OtlpLogController}
- */
-@ExtendWith(MockitoExtension.class)
-class OtlpLogControllerTest {
-
- private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf";
-
- private MockMvc mockMvc;
-
- @Mock
- private OtlpLogProtocolAdapter otlpLogProtocolAdapter;
-
- private OtlpLogController otlpLogController;
-
- @BeforeEach
- void setUp() {
- this.otlpLogController = new OtlpLogController(otlpLogProtocolAdapter);
- this.mockMvc = MockMvcBuilders.standaloneSetup(otlpLogController).build();
- }
-
- @Test
- void testIngestJsonLogsSuccess() throws Exception {
- String jsonContent = "{\"resourceLogs\":[]}";
-
- doNothing().when(otlpLogProtocolAdapter).ingest(anyString());
-
- mockMvc.perform(
- MockMvcRequestBuilders
- .post("/api/logs/otlp/v1/logs")
- .contentType(MediaType.APPLICATION_JSON)
- .content(jsonContent)
- )
- .andDo(print())
- .andExpect(status().isOk())
- .andExpect(content().contentType(MediaType.APPLICATION_JSON))
- .andReturn();
- }
-
- @Test
- void testIngestJsonLogsFailure() throws Exception {
- String jsonContent = "{\"invalid\":\"content\"}";
-
- doThrow(new IllegalArgumentException("Invalid OTLP JSON log content"))
- .when(otlpLogProtocolAdapter).ingest(anyString());
-
- mockMvc.perform(
- MockMvcRequestBuilders
- .post("/api/logs/otlp/v1/logs")
- .contentType(MediaType.APPLICATION_JSON)
- .content(jsonContent)
- )
- .andExpect(status().isBadRequest())
- .andExpect(content().contentType(MediaType.APPLICATION_JSON))
- .andReturn();
- }
-
- @Test
- void testIngestBinaryLogsSuccess() throws Exception {
- byte[] binaryContent = new byte[]{0x0a, 0x0b, 0x0c};
-
- doNothing().when(otlpLogProtocolAdapter).ingestBinary(any(byte[].class));
-
- mockMvc.perform(
- MockMvcRequestBuilders
- .post("/api/logs/otlp/v1/logs")
- .contentType(CONTENT_TYPE_PROTOBUF)
- .content(binaryContent)
- )
- .andDo(print())
- .andExpect(status().isOk())
- .andExpect(content().contentType(CONTENT_TYPE_PROTOBUF))
- .andReturn();
- }
-
- @Test
- void testIngestBinaryLogsFailure() throws Exception {
- byte[] binaryContent = new byte[]{0x0a, 0x0b, 0x0c};
-
- doThrow(new IllegalArgumentException("Invalid OTLP binary log content"))
- .when(otlpLogProtocolAdapter).ingestBinary(any(byte[].class));
-
- mockMvc.perform(
- MockMvcRequestBuilders
- .post("/api/logs/otlp/v1/logs")
- .contentType(CONTENT_TYPE_PROTOBUF)
- .content(binaryContent)
- )
- .andExpect(status().isBadRequest())
- .andExpect(content().contentType(CONTENT_TYPE_PROTOBUF))
- .andReturn();
- }
-}
diff --git a/hertzbeat-manager/pom.xml b/hertzbeat-manager/pom.xml
index 57994eac2d9..f98eb08c521 100644
--- a/hertzbeat-manager/pom.xml
+++ b/hertzbeat-manager/pom.xml
@@ -91,10 +91,10 @@
org.apache.hertzbeathertzbeat-otel
-
+
org.apache.hertzbeat
- hertzbeat-log
+ hertzbeat-observability
diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/RestTemplateConfigTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/RestTemplateConfigTest.java
new file mode 100644
index 00000000000..99c0e84a805
--- /dev/null
+++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/RestTemplateConfigTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.manager.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import java.net.http.HttpClient;
+import org.apache.hertzbeat.common.constants.NetworkConstants;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.JdkClientHttpRequestFactory;
+import org.springframework.test.util.ReflectionTestUtils;
+
+class RestTemplateConfigTest {
+
+ @Test
+ void greptimeQueryRequestFactoryUsesDedicatedTimeouts() {
+ ClientHttpRequestFactory factory = new RestTemplateConfig().greptimeQueryClientHttpRequestFactory();
+
+ JdkClientHttpRequestFactory jdkFactory = assertInstanceOf(JdkClientHttpRequestFactory.class, factory);
+ assertEquals(NetworkConstants.HttpClientConstants.GREPTIME_QUERY_READ_TIMEOUT,
+ ReflectionTestUtils.getField(jdkFactory, "readTimeout"));
+
+ HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(jdkFactory, "httpClient");
+ assertEquals(NetworkConstants.HttpClientConstants.GREPTIME_QUERY_CONNECT_TIMEOUT,
+ httpClient.connectTimeout().orElseThrow());
+ }
+}
diff --git a/hertzbeat-manager/src/test/resources/sureness.yml b/hertzbeat-manager/src/test/resources/sureness.yml
index dff4e65966f..c11eabc1568 100644
--- a/hertzbeat-manager/src/test/resources/sureness.yml
+++ b/hertzbeat-manager/src/test/resources/sureness.yml
@@ -80,12 +80,12 @@ resourceRole:
- /api/ai/**===post===[admin]
- /api/ai/**===put===[admin]
- /api/ai/**===delete===[admin]
- - /api/logs/sse/**===get===[admin,user,guest]
+ - /api/otlp/v1/**===post===[admin,user]
+ # deprecated 1.8.x OTLP log aliases, forwarded to /api/otlp/v1/logs, removed in 2.0
+ - /api/logs/otlp/**===post===[admin,user]
- /api/logs/ingest/**===post===[admin,user]
- - /api/otlp/**===post===[admin,user]
- - /api/ingestion/otlp/**===get===[admin,user,guest]
- - /api/logs/**===get===[admin,user,guest]
- - /api/traces/**===get===[admin,user,guest]
+ - /api/observability/logs===delete===[admin]
+ - /api/observability/**===get===[admin,user,guest]
# The OpenAPI document is a map of every route, parameter and model, so it is
# scoped like any other administrative resource instead of being anonymous
- /v3/api-docs/**===get===[admin]
@@ -102,7 +102,6 @@ excludedResource:
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
- - /api/observability/capability===get
- /api/push/**===*
- /api/status/page/public/**===*
- /api/manager/sse/**===*
diff --git a/hertzbeat-log/pom.xml b/hertzbeat-observability/pom.xml
similarity index 98%
rename from hertzbeat-log/pom.xml
rename to hertzbeat-observability/pom.xml
index c5c432aca5f..5d62967651d 100644
--- a/hertzbeat-log/pom.xml
+++ b/hertzbeat-observability/pom.xml
@@ -25,7 +25,7 @@
4.0.0
- hertzbeat-log
+ hertzbeat-observability${project.artifactId}
diff --git a/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/config/LogAutoConfiguration.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/config/ObservabilityAutoConfiguration.java
similarity index 72%
rename from hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/config/LogAutoConfiguration.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/config/ObservabilityAutoConfiguration.java
index f58973b12dc..66e0a0c4dae 100644
--- a/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/config/LogAutoConfiguration.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/config/ObservabilityAutoConfiguration.java
@@ -15,21 +15,19 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.otel.config;
+package org.apache.hertzbeat.observability.config;
import org.apache.hertzbeat.common.constants.ConfigConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
-import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
/**
- * Log auto configuration.
+ * Entity-free observability module auto configuration.
*/
+@Configuration(proxyBeanMethods = false)
@ComponentScan(basePackages = ConfigConstants.PkgConstant.PKG
- + SignConstants.DOT
- + ConfigConstants.FunctionModuleConstants.LOG
-)
-@EnableConfigurationProperties(GreptimeProperties.class)
-public class LogAutoConfiguration {
+ + SignConstants.DOT
+ + ConfigConstants.FunctionModuleConstants.OBSERVABILITY)
+public class ObservabilityAutoConfiguration {
}
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/config/OtlpGrpcServerConfig.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/config/OtlpGrpcServerConfig.java
similarity index 91%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/config/OtlpGrpcServerConfig.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/config/OtlpGrpcServerConfig.java
index 9e6d3bc067c..d98e4d8512d 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/config/OtlpGrpcServerConfig.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/config/OtlpGrpcServerConfig.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.config;
+package org.apache.hertzbeat.observability.config;
import io.grpc.ForwardingServerCallListener;
import io.grpc.Metadata;
@@ -41,10 +41,11 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.security.OtlpAccessTokenValidator;
-import org.apache.hertzbeat.log.service.OtlpSignalForwarder;
-import org.apache.hertzbeat.log.service.SignalQueryRejectedException;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard.Workload;
+import org.apache.hertzbeat.observability.service.OtlpLogIngestionService;
+import org.apache.hertzbeat.observability.service.OtlpSignalForwarder;
+import org.apache.hertzbeat.observability.service.SignalQueryRejectedException;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard.Workload;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -64,12 +65,14 @@ public OtlpGrpcServerRunner otlpGrpcServerRunner(
@Value("${hertzbeat.otlp.grpc.host:0.0.0.0}") String host,
@Value("${hertzbeat.otlp.grpc.port:4317}") int port,
OtlpSignalForwarder signalForwarder,
+ OtlpLogIngestionService logIngestionService,
SignalWorkloadGuard workloadGuard,
ObjectProvider tokenValidatorProvider) {
OtlpAccessTokenValidator validator = tokenValidatorProvider.getIfAvailable(
() -> token -> "OTLP token validation is unavailable");
ServerInterceptor interceptor = new BearerTokenInterceptor(validator);
- return new OtlpGrpcServerRunner(host, port, signalForwarder, workloadGuard, interceptor);
+ return new OtlpGrpcServerRunner(host, port, signalForwarder, logIngestionService,
+ workloadGuard, interceptor);
}
@Slf4j
@@ -78,6 +81,7 @@ static final class OtlpGrpcServerRunner {
private final String host;
private final int port;
private final OtlpSignalForwarder signalForwarder;
+ private final OtlpLogIngestionService logIngestionService;
private final SignalWorkloadGuard workloadGuard;
private final ServerInterceptor authInterceptor;
private Server server;
@@ -86,7 +90,7 @@ public void start() throws IOException {
server = NettyServerBuilder.forAddress(new InetSocketAddress(host, port))
.addService(ServerInterceptors.intercept(new MetricsService(signalForwarder, workloadGuard),
authInterceptor))
- .addService(ServerInterceptors.intercept(new LogsService(signalForwarder, workloadGuard),
+ .addService(ServerInterceptors.intercept(new LogsService(logIngestionService, workloadGuard),
authInterceptor))
.addService(ServerInterceptors.intercept(new TracesService(signalForwarder, workloadGuard),
authInterceptor))
@@ -123,14 +127,14 @@ public void export(ExportMetricsServiceRequest request,
@RequiredArgsConstructor
static final class LogsService extends LogsServiceGrpc.LogsServiceImplBase {
- private final OtlpSignalForwarder signalForwarder;
+ private final OtlpLogIngestionService logIngestionService;
private final SignalWorkloadGuard workloadGuard;
@Override
public void export(ExportLogsServiceRequest request, StreamObserver observer) {
try {
byte[] response = workloadGuard.execute(Workload.OTLP_WRITE,
- () -> signalForwarder.forwardProtobuf("logs", request.toByteArray()));
+ () -> logIngestionService.ingestProtobuf(request.toByteArray()));
observer.onNext(response.length == 0 ? ExportLogsServiceResponse.getDefaultInstance()
: ExportLogsServiceResponse.parseFrom(response));
observer.onCompleted();
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LegacyOtlpLogRouteController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LegacyOtlpLogRouteController.java
new file mode 100644
index 00000000000..cb68f9d8cbe
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LegacyOtlpLogRouteController.java
@@ -0,0 +1,126 @@
+/*
+ * 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.observability.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.nio.charset.StandardCharsets;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.observability.service.OtlpLogIngestionService;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard.Workload;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Deprecated 1.8.x OTLP log ingestion routes kept as aliases of {@code POST /api/otlp/v1/logs}.
+ *
+ *
HertzBeat 1.8.0 documented {@code POST /api/logs/otlp/v1/logs} and also exposed
+ * {@code POST /api/logs/ingest/{protocol}}. Collectors and SDKs configured against those paths keep
+ * working on 1.9.x through this alias, which forwards to the same log fan-out as the canonical
+ * route and stamps a {@code Deprecation} / {@code Link} header on every response.
+ *
+ *
These aliases are scheduled for removal in HertzBeat 2.0. New integrations must use
+ * {@code /api/otlp/v1/logs}.
+ *
+ * @deprecated since 1.9.0, use {@code POST /api/otlp/v1/logs}
+ */
+@Deprecated(since = "1.9.0", forRemoval = true)
+@Slf4j
+@RestController
+@Tag(name = "OTLP Log Controller (deprecated 1.8 aliases)")
+public class LegacyOtlpLogRouteController {
+
+ /**
+ * Canonical route the aliases forward to.
+ */
+ public static final String CANONICAL_LOGS_ROUTE = "/api/otlp/v1/logs";
+
+ /**
+ * 1.8.0 documented OTLP/HTTP log route.
+ */
+ public static final String LEGACY_OTLP_LOGS_ROUTE = "/api/logs/otlp/v1/logs";
+
+ /**
+ * 1.8.0 protocol-named log route; only the {@code otlp} protocol ever had an adapter.
+ */
+ public static final String LEGACY_INGEST_ROUTE = "/api/logs/ingest/{protocol}";
+
+ private static final String OTLP_PROTOCOL = "otlp";
+
+ private static final String DEPRECATION_HEADER = "Deprecation";
+ private static final String LINK_HEADER = "Link";
+ private static final String LINK_VALUE = "<" + CANONICAL_LOGS_ROUTE + ">; rel=\"successor-version\"";
+
+ private final OtlpLogIngestionService logIngestionService;
+ private final SignalWorkloadGuard workloadGuard;
+
+ public LegacyOtlpLogRouteController(OtlpLogIngestionService logIngestionService,
+ SignalWorkloadGuard workloadGuard) {
+ this.logIngestionService = logIngestionService;
+ this.workloadGuard = workloadGuard;
+ }
+
+ @PostMapping(LEGACY_OTLP_LOGS_ROUTE)
+ @Operation(summary = "Deprecated alias of POST /api/otlp/v1/logs", deprecated = true)
+ public ResponseEntity legacyOtlpLogs(@RequestBody byte[] content, @RequestHeader HttpHeaders headers) {
+ return forward(LEGACY_OTLP_LOGS_ROUTE, content, headers);
+ }
+
+ @PostMapping(LEGACY_INGEST_ROUTE)
+ @Operation(summary = "Deprecated alias of POST /api/otlp/v1/logs (otlp protocol only)", deprecated = true)
+ public ResponseEntity legacyIngest(@PathVariable("protocol") String protocol,
+ @RequestBody byte[] content,
+ @RequestHeader HttpHeaders headers) {
+ if (!OTLP_PROTOCOL.equalsIgnoreCase(protocol)) {
+ log.warn("Deprecated route /api/logs/ingest/{} rejected: only the otlp protocol is supported", protocol);
+ return deprecated(ResponseEntity.status(HttpStatus.BAD_REQUEST))
+ .body(("Not support the " + protocol + " protocol log, use " + CANONICAL_LOGS_ROUTE)
+ .getBytes(StandardCharsets.UTF_8));
+ }
+ return forward("/api/logs/ingest/" + OTLP_PROTOCOL, content, headers);
+ }
+
+ private ResponseEntity forward(String legacyRoute, byte[] content, HttpHeaders headers) {
+ log.warn("Deprecated OTLP log route {} was called; migrate the exporter to {} before HertzBeat 2.0",
+ legacyRoute, CANONICAL_LOGS_ROUTE);
+ ResponseEntity canonical = workloadGuard.execute(Workload.OTLP_WRITE,
+ () -> logIngestionService.ingestHttp(content, headers));
+ return deprecated(ResponseEntity.status(canonical.getStatusCode()).headers(canonical.getHeaders()))
+ .body(canonical.getBody());
+ }
+
+ private static ResponseEntity.BodyBuilder deprecated(ResponseEntity.BodyBuilder builder) {
+ return builder
+ .header(DEPRECATION_HEADER, "true")
+ .header(LINK_HEADER, LINK_VALUE);
+ }
+
+ @ExceptionHandler(IllegalArgumentException.class)
+ public ResponseEntity invalidPayload(IllegalArgumentException exception) {
+ return deprecated(ResponseEntity.status(HttpStatus.BAD_REQUEST))
+ .body(exception.getMessage().getBytes(StandardCharsets.UTF_8));
+ }
+}
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogManagerController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LogManagerController.java
similarity index 95%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogManagerController.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LogManagerController.java
index f552e5c20f1..8d9fbe506f7 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogManagerController.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LogManagerController.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -37,7 +37,7 @@
* Controller for managing log entries in HertzBeat.
*/
@RestController
-@RequestMapping(path = "/api/logs", produces = "application/json")
+@RequestMapping(path = "/api/observability/logs", produces = "application/json")
@Tag(name = "Log Management Controller")
@Slf4j
public class LogManagerController {
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogQueryController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LogQueryController.java
similarity index 97%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogQueryController.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LogQueryController.java
index 7cde958b437..a5e18c1c8cd 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogQueryController.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LogQueryController.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -37,8 +37,8 @@
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.dto.observability.LogQueryFilter;
import org.apache.hertzbeat.common.entity.log.LogEntry;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard.Workload;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard.Workload;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -49,7 +49,7 @@
* Log query and statistics APIs for UI consumption
*/
@RestController
-@RequestMapping(path = "/api/logs", produces = "application/json")
+@RequestMapping(path = "/api/observability/logs", produces = "application/json")
@Tag(name = "Log Query Controller")
@Slf4j
public class LogQueryController {
@@ -67,7 +67,7 @@ public LogQueryController(HistoryDataReader historyDataReader, SignalWorkloadGua
this.workloadGuard = workloadGuard;
}
- @GetMapping("/list")
+ @GetMapping
@Operation(summary = "Query logs by time range with optional filters",
description = "Query logs by [start,end] in ms and optional filters with pagination. Returns paginated log entries sorted by timestamp in descending order.")
public ResponseEntity>> list(
@@ -101,7 +101,7 @@ public ResponseEntity>> list(
});
}
- @GetMapping("/stats/overview")
+ @GetMapping("/overview")
@Operation(summary = "Log overview statistics",
description = "Overall counts and basic statistics with filters. Provides counts by severity levels according to OpenTelemetry standard.")
public ResponseEntity>> overviewStats(
@@ -132,7 +132,7 @@ private ResponseEntity>> doOverviewStats(LogQueryFil
return ResponseEntity.ok(Message.success(historyDataReader.queryLogOverviewAggregate(filter)));
}
- @GetMapping("/stats/trace-coverage")
+ @GetMapping("/trace-coverage")
@Operation(summary = "Trace coverage statistics",
description = "Statistics about trace information availability. Shows how many logs have trace IDs, span IDs, or both for distributed tracing analysis.")
public ResponseEntity>> traceCoverageStats(
@@ -165,7 +165,7 @@ private ResponseEntity>> doTraceCoverageStats(LogQue
return ResponseEntity.ok(Message.success(result));
}
- @GetMapping("/stats/trend")
+ @GetMapping("/trend")
@Operation(summary = "Log trend over time",
description = "Count logs by hour intervals with filters. Groups logs by hour and provides time-series data for trend analysis.")
public ResponseEntity>> trendStats(
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogSseController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LogSseController.java
similarity index 87%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogSseController.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LogSseController.java
index e519ae06ee2..70956167616 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogSseController.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/LogSseController.java
@@ -17,12 +17,12 @@
* under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
-import org.apache.hertzbeat.log.notice.LogSseFilterCriteria;
-import org.apache.hertzbeat.log.notice.LogSseManager;
+import org.apache.hertzbeat.observability.notice.LogSseFilterCriteria;
+import org.apache.hertzbeat.observability.notice.LogSseManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -34,7 +34,7 @@
* SSE controller for log streaming with filtering support
*/
@RestController
-@RequestMapping(path = "/api/logs/sse", produces = {TEXT_EVENT_STREAM_VALUE})
+@RequestMapping(path = "/api/observability/logs", produces = {TEXT_EVENT_STREAM_VALUE})
public class LogSseController {
private final LogSseManager emitterManager;
@@ -48,10 +48,10 @@ public LogSseController(LogSseManager emitterManager) {
* @param filterCriteria Filter criteria for log events (all parameters are optional)
* @return SSE emitter for streaming log events
*/
- @GetMapping(path = "/subscribe")
+ @GetMapping(path = "/stream")
@Operation(summary = "Subscribe to log events with optional filtering", description = "Subscribe to log events with optional filtering")
public SseEmitter subscribe(@ModelAttribute LogSseFilterCriteria filterCriteria) {
Long clientId = SnowFlakeIdGenerator.generateId();
return emitterManager.createEmitter(clientId, filterCriteria);
}
-}
\ No newline at end of file
+}
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/ObservabilityCapabilityController.java
similarity index 96%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityController.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/ObservabilityCapabilityController.java
index 055167f4030..9943c2f69da 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityController.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/ObservabilityCapabilityController.java
@@ -15,13 +15,13 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.dto.observability.ObservabilityCapability;
-import org.apache.hertzbeat.log.service.ThreeSignalQueryService;
+import org.apache.hertzbeat.warehouse.service.ThreeSignalQueryService;
import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.http.ResponseEntity;
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/OtlpLogController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/OtlpLogController.java
new file mode 100644
index 00000000000..8e031da9c8d
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/OtlpLogController.java
@@ -0,0 +1,62 @@
+/*
+ * 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.observability.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.nio.charset.StandardCharsets;
+import org.apache.hertzbeat.observability.service.OtlpLogIngestionService;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard.Workload;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/** Canonical OTLP/HTTP log endpoint backed by the HertzBeat log fan-out. */
+@RestController
+@RequestMapping("/api/otlp/v1")
+@Tag(name = "OTLP Log Controller")
+public class OtlpLogController {
+
+ private final OtlpLogIngestionService logIngestionService;
+ private final SignalWorkloadGuard workloadGuard;
+
+ public OtlpLogController(OtlpLogIngestionService logIngestionService, SignalWorkloadGuard workloadGuard) {
+ this.logIngestionService = logIngestionService;
+ this.workloadGuard = workloadGuard;
+ }
+
+ @PostMapping("/logs")
+ @Operation(summary = "Ingest OTLP logs")
+ public ResponseEntity logs(@RequestBody byte[] content, @RequestHeader HttpHeaders headers) {
+ return workloadGuard.execute(Workload.OTLP_WRITE,
+ () -> logIngestionService.ingestHttp(content, headers));
+ }
+
+ @ExceptionHandler(IllegalArgumentException.class)
+ public ResponseEntity invalidPayload(IllegalArgumentException exception) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+ .body(exception.getMessage().getBytes(StandardCharsets.UTF_8));
+ }
+}
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/OtlpSignalController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/OtlpSignalController.java
similarity index 86%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/OtlpSignalController.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/OtlpSignalController.java
index a88e2d10eb5..fbca7708b1e 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/OtlpSignalController.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/OtlpSignalController.java
@@ -15,14 +15,14 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.nio.charset.StandardCharsets;
-import org.apache.hertzbeat.log.service.OtlpSignalForwarder;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard.Workload;
+import org.apache.hertzbeat.observability.service.OtlpSignalForwarder;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard.Workload;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@@ -55,12 +55,6 @@ public ResponseEntity metrics(@RequestBody byte[] content, @RequestHeade
return forward("metrics", content, headers);
}
- @PostMapping("/logs")
- @Operation(summary = "Ingest OTLP logs")
- public ResponseEntity logs(@RequestBody byte[] content, @RequestHeader HttpHeaders headers) {
- return forward("logs", content, headers);
- }
-
@PostMapping("/traces")
@Operation(summary = "Ingest OTLP traces")
public ResponseEntity traces(@RequestBody byte[] content, @RequestHeader HttpHeaders headers) {
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/SignalWorkloadExceptionHandler.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/SignalWorkloadExceptionHandler.java
similarity index 93%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/SignalWorkloadExceptionHandler.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/SignalWorkloadExceptionHandler.java
index 3cac628fc8a..80508a6fb21 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/SignalWorkloadExceptionHandler.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/SignalWorkloadExceptionHandler.java
@@ -15,13 +15,13 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.support.exception.StorageUnavailableException;
-import org.apache.hertzbeat.log.service.SignalQueryRejectedException;
+import org.apache.hertzbeat.observability.service.SignalQueryRejectedException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -32,7 +32,7 @@
import org.springframework.web.client.RestClientException;
/** Maps bounded signal workload rejection to a retryable HTTP response. */
-@RestControllerAdvice(basePackages = "org.apache.hertzbeat.log.controller")
+@RestControllerAdvice(basePackages = "org.apache.hertzbeat.observability.controller")
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SignalWorkloadExceptionHandler {
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/ThreeSignalQueryController.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/ThreeSignalQueryController.java
similarity index 91%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/ThreeSignalQueryController.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/ThreeSignalQueryController.java
index de95425c17b..70be734dadd 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/ThreeSignalQueryController.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/controller/ThreeSignalQueryController.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -26,19 +26,21 @@
import org.apache.hertzbeat.common.entity.dto.observability.TraceDetail;
import org.apache.hertzbeat.common.entity.dto.observability.TraceListItem;
import org.apache.hertzbeat.common.entity.dto.observability.TraceOverview;
-import org.apache.hertzbeat.log.service.ThreeSignalQueryService;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard.Workload;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard.Workload;
+import org.apache.hertzbeat.warehouse.service.ThreeSignalQueryService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** Public entity-free query APIs for OTLP metrics and traces. */
@RestController
@ConditionalOnProperty(prefix = "warehouse.store.greptime", name = "enabled", havingValue = "true")
+@RequestMapping(path = "/api/observability", produces = "application/json")
@Tag(name = "Three Signal Query Controller")
public class ThreeSignalQueryController {
@@ -50,7 +52,7 @@ public ThreeSignalQueryController(ThreeSignalQueryService queryService, SignalWo
this.workloadGuard = workloadGuard;
}
- @GetMapping(path = "/api/ingestion/otlp/metrics/console", produces = "application/json")
+ @GetMapping("/metrics/query")
@Operation(summary = "Query OTLP metrics")
public ResponseEntity> metrics(
@RequestParam(value = "query", required = false) String query,
@@ -69,7 +71,7 @@ public ResponseEntity> metrics(
filter, groupBy, aggregation, step, operationName))));
}
- @GetMapping(path = "/api/ingestion/otlp/metrics/inventory", produces = "application/json")
+ @GetMapping("/metrics/inventory")
@Operation(summary = "List OTLP metric names")
public ResponseEntity> metricInventory(
@RequestParam(value = "start", required = false) Long start,
@@ -82,7 +84,7 @@ public ResponseEntity> metricInventory(
queryService.metricInventory(start, end, serviceName, serviceNamespace, environment, limit))));
}
- @GetMapping(path = "/api/traces/list", produces = "application/json")
+ @GetMapping("/traces")
@Operation(summary = "Query traces")
public ResponseEntity>> traces(
@RequestParam(value = "start", required = false) Long start,
@@ -102,7 +104,7 @@ public ResponseEntity>> traces(
environment, operationName, minDurationMs, maxDurationMs, pageIndex, pageSize))));
}
- @GetMapping(path = "/api/traces/stats/overview", produces = "application/json")
+ @GetMapping("/traces/overview")
@Operation(summary = "Get trace overview")
public ResponseEntity> traceOverview(
@RequestParam(value = "start", required = false) Long start,
@@ -120,14 +122,14 @@ public ResponseEntity> traceOverview(
environment, operationName, minDurationMs, maxDurationMs))));
}
- @GetMapping(path = "/api/traces/{traceId}", produces = "application/json")
+ @GetMapping("/traces/{traceId}")
@Operation(summary = "Get trace detail")
public ResponseEntity> traceDetail(@PathVariable("traceId") String traceId) {
return workloadGuard.execute(Workload.TRACES,
() -> ResponseEntity.ok(Message.success(queryService.traceDetail(traceId))));
}
- @GetMapping(path = "/api/traces/{traceId}/spans", produces = "application/json")
+ @GetMapping("/traces/{traceId}/spans")
@Operation(summary = "Get trace spans")
public ResponseEntity> traceSpans(@PathVariable("traceId") String traceId) {
return workloadGuard.execute(Workload.TRACES,
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteria.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/notice/LogSseFilterCriteria.java
similarity index 98%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteria.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/notice/LogSseFilterCriteria.java
index b42f68aa8a8..6e476c1f3f7 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteria.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/notice/LogSseFilterCriteria.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.hertzbeat.log.notice;
+package org.apache.hertzbeat.observability.notice;
import lombok.Data;
import lombok.AllArgsConstructor;
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseManager.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/notice/LogSseManager.java
similarity index 99%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseManager.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/notice/LogSseManager.java
index 78d499cc45d..b8def56026f 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/notice/LogSseManager.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/notice/LogSseManager.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.hertzbeat.log.notice;
+package org.apache.hertzbeat.observability.notice;
import jakarta.annotation.PreDestroy;
import lombok.AllArgsConstructor;
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/OtlpLogIngestionService.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/OtlpLogIngestionService.java
new file mode 100644
index 00000000000..c351fd34748
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/OtlpLogIngestionService.java
@@ -0,0 +1,44 @@
+/*
+ * 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.observability.service;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+
+/**
+ * Canonical OTLP log ingestion boundary shared by the HTTP and gRPC transports.
+ */
+public interface OtlpLogIngestionService {
+
+ /**
+ * Decode and ingest an OTLP/HTTP log request.
+ *
+ * @param content encoded OTLP request body
+ * @param headers request headers
+ * @return an OTLP response encoded for the request content type
+ */
+ ResponseEntity ingestHttp(byte[] content, HttpHeaders headers);
+
+ /**
+ * Decode and ingest an OTLP/gRPC protobuf request.
+ *
+ * @param content encoded {@code ExportLogsServiceRequest}
+ * @return encoded {@code ExportLogsServiceResponse}
+ */
+ byte[] ingestProtobuf(byte[] content);
+}
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/OtlpSignalForwarder.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/OtlpSignalForwarder.java
similarity index 95%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/OtlpSignalForwarder.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/OtlpSignalForwarder.java
index b2305fafc3d..1cf555b8d05 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/OtlpSignalForwarder.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/OtlpSignalForwarder.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.service;
+package org.apache.hertzbeat.observability.service;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/SignalQueryRejectedException.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/SignalQueryRejectedException.java
similarity index 95%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/SignalQueryRejectedException.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/SignalQueryRejectedException.java
index 40f43b1cbb3..7e1d9850078 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/SignalQueryRejectedException.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/SignalQueryRejectedException.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.service;
+package org.apache.hertzbeat.observability.service;
/** Raised when a signal workload exceeds its isolated concurrency budget. */
public class SignalQueryRejectedException extends RuntimeException {
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/SignalWorkloadGuard.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/SignalWorkloadGuard.java
similarity index 98%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/SignalWorkloadGuard.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/SignalWorkloadGuard.java
index f60677ad151..0d2ada58dee 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/SignalWorkloadGuard.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/SignalWorkloadGuard.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.service;
+package org.apache.hertzbeat.observability.service;
import java.time.Duration;
import java.util.EnumMap;
diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/impl/DefaultOtlpLogIngestionService.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/impl/DefaultOtlpLogIngestionService.java
new file mode 100644
index 00000000000..4dbc8fef878
--- /dev/null
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/impl/DefaultOtlpLogIngestionService.java
@@ -0,0 +1,92 @@
+/*
+ * 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.observability.service.impl;
+
+import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.zip.GZIPInputStream;
+import org.apache.hertzbeat.observability.service.OtlpLogIngestionService;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+
+/** Default OTLP log ingestion service backed by the HertzBeat log fan-out. */
+@Service
+public class DefaultOtlpLogIngestionService implements OtlpLogIngestionService {
+
+ private static final MediaType PROTOBUF = MediaType.parseMediaType("application/x-protobuf");
+ private static final byte[] JSON_RESPONSE = "{}".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] PROTOBUF_RESPONSE = ExportLogsServiceResponse.getDefaultInstance().toByteArray();
+
+ private final OtlpLogProtocolAdapter protocolAdapter;
+
+ public DefaultOtlpLogIngestionService(OtlpLogProtocolAdapter protocolAdapter) {
+ this.protocolAdapter = protocolAdapter;
+ }
+
+ @Override
+ public ResponseEntity ingestHttp(byte[] content, HttpHeaders headers) {
+ HttpHeaders safeHeaders = headers == null ? HttpHeaders.EMPTY : headers;
+ byte[] normalizedContent = maybeDecompress(content, safeHeaders);
+ MediaType contentType = safeHeaders.getContentType();
+ if (contentType != null && MediaType.APPLICATION_JSON.includes(contentType)) {
+ protocolAdapter.ingest(new String(normalizedContent, StandardCharsets.UTF_8));
+ return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(JSON_RESPONSE);
+ }
+ protocolAdapter.ingestBinary(normalizedContent);
+ return ResponseEntity.ok().contentType(PROTOBUF).body(PROTOBUF_RESPONSE);
+ }
+
+ @Override
+ public byte[] ingestProtobuf(byte[] content) {
+ protocolAdapter.ingestBinary(content);
+ return PROTOBUF_RESPONSE;
+ }
+
+ private byte[] maybeDecompress(byte[] content, HttpHeaders headers) {
+ byte[] safeContent = content == null ? new byte[0] : content;
+ List encodings = headers.get(HttpHeaders.CONTENT_ENCODING);
+ if (encodings == null || encodings.stream().noneMatch(this::containsGzipEncoding)) {
+ return safeContent;
+ }
+ try (GZIPInputStream input = new GZIPInputStream(new ByteArrayInputStream(safeContent));
+ ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+ input.transferTo(output);
+ return output.toByteArray();
+ } catch (IOException exception) {
+ throw new IllegalArgumentException("Malformed gzip OTLP log payload", exception);
+ }
+ }
+
+ private boolean containsGzipEncoding(String value) {
+ if (value == null) {
+ return false;
+ }
+ for (String encoding : value.split(",")) {
+ if ("gzip".equalsIgnoreCase(encoding.trim())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/GreptimeOtlpSignalForwarder.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/impl/GreptimeOtlpSignalForwarder.java
similarity index 62%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/GreptimeOtlpSignalForwarder.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/impl/GreptimeOtlpSignalForwarder.java
index 766c58f1eca..0fb24865441 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/GreptimeOtlpSignalForwarder.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/impl/GreptimeOtlpSignalForwarder.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.service.impl;
+package org.apache.hertzbeat.observability.service.impl;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
@@ -34,20 +34,15 @@
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
-import org.apache.hertzbeat.log.service.OtlpSignalForwarder;
+import org.apache.hertzbeat.observability.service.OtlpSignalForwarder;
import org.apache.hertzbeat.common.util.JsonUtil;
-import org.apache.hertzbeat.warehouse.constants.WarehouseConstants;
-import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
+import org.apache.hertzbeat.warehouse.service.OtlpSignalStorage;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
-import org.springframework.web.client.RestTemplate;
/** Greptime native OTLP forwarder for metrics, logs, and traces. */
@Service
@@ -55,25 +50,11 @@
public class GreptimeOtlpSignalForwarder implements OtlpSignalForwarder {
private static final String PROTOBUF = "application/x-protobuf";
- private static final String GREPTIME_DATABASE_HEADER = "X-Greptime-DB-Name";
- private static final String GREPTIME_TRACE_TABLE_HEADER = "X-Greptime-Trace-Table-Name";
- private static final String GREPTIME_PIPELINE_HEADER = "X-Greptime-Pipeline-Name";
- private static final String GREPTIME_LOG_TABLE_HEADER = "X-Greptime-Log-Table-Name";
- private static final String GREPTIME_LOG_PIPELINE_HEADER = "X-Greptime-Log-Pipeline-Name";
- private static final String GREPTIME_PROMOTE_RESOURCE_HEADER =
- "X-Greptime-OTLP-Metric-Promote-Resource-Attrs";
- private static final String PROMOTED_RESOURCE_ATTRIBUTES = String.join(";", List.of(
- "service.name", "service.namespace", "service.version", "deployment.environment.name",
- "host.name", "k8s.namespace.name", "k8s.pod.name"));
private static final Set SIGNALS = Set.of("metrics", "logs", "traces");
- private final GreptimeProperties greptimeProperties;
- private final RestTemplate restTemplate;
+ private final OtlpSignalStorage signalStorage;
- public GreptimeOtlpSignalForwarder(GreptimeProperties greptimeProperties,
- @Qualifier(WarehouseConstants.GREPTIME_WRITE_REST_TEMPLATE)
- RestTemplate restTemplate) {
- this.greptimeProperties = greptimeProperties;
- this.restTemplate = restTemplate;
+ public GreptimeOtlpSignalForwarder(OtlpSignalStorage signalStorage) {
+ this.signalStorage = signalStorage;
}
@Override
@@ -81,10 +62,11 @@ public ResponseEntity forwardHttp(String signal, byte[] content, HttpHea
HttpHeaders safeHeaders = requestHeaders == null ? new HttpHeaders() : requestHeaders;
byte[] normalized = maybeDecompress(content, safeHeaders);
MediaType contentType = safeHeaders.getContentType();
- byte[] protobuf = contentType != null && MediaType.APPLICATION_JSON.includes(contentType)
- ? jsonToProtobuf(signal, normalized) : validateProtobuf(signal, normalized);
- byte[] response = forwardProtobuf(signal, protobuf);
- if (contentType != null && MediaType.APPLICATION_JSON.includes(contentType)) {
+ boolean json = contentType != null && MediaType.APPLICATION_JSON.includes(contentType);
+ // Both branches yield already-validated protobuf, so write directly instead of re-parsing via forwardProtobuf.
+ byte[] protobuf = json ? jsonToProtobuf(signal, normalized) : validateProtobuf(signal, normalized);
+ byte[] response = signalStorage.writeProtobuf(signal, protobuf);
+ if (json) {
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON)
.body("{}".getBytes(StandardCharsets.UTF_8));
}
@@ -93,40 +75,8 @@ public ResponseEntity forwardHttp(String signal, byte[] content, HttpHea
@Override
public byte[] forwardProtobuf(String signal, byte[] content) {
- String normalizedSignal = normalizeSignal(signal);
- byte[] validContent = validateProtobuf(normalizedSignal, content);
- HttpHeaders headers = greptimeHeaders(normalizedSignal);
- ResponseEntity response = restTemplate.exchange(
- endpoint(greptimeProperties.httpEndpoint(), "/v1/otlp/v1/" + normalizedSignal),
- HttpMethod.POST,
- new HttpEntity<>(validContent, headers),
- byte[].class);
- if (!response.getStatusCode().is2xxSuccessful()) {
- throw new IllegalStateException("GreptimeDB rejected OTLP " + normalizedSignal);
- }
- return response.getBody() == null ? new byte[0] : response.getBody();
- }
-
- private HttpHeaders greptimeHeaders(String signal) {
- HttpHeaders headers = new HttpHeaders();
- headers.setContentType(MediaType.parseMediaType(PROTOBUF));
- headers.setAccept(List.of(MediaType.parseMediaType(PROTOBUF)));
- headers.set(GREPTIME_DATABASE_HEADER, StringUtils.hasText(greptimeProperties.database())
- ? greptimeProperties.database() : "public");
- if ("metrics".equals(signal)) {
- headers.set(GREPTIME_PROMOTE_RESOURCE_HEADER, PROMOTED_RESOURCE_ATTRIBUTES);
- } else if ("traces".equals(signal)) {
- headers.set(GREPTIME_TRACE_TABLE_HEADER, "hzb_traces");
- headers.set(GREPTIME_PIPELINE_HEADER, "greptime_trace_v1");
- } else {
- headers.set(GREPTIME_LOG_TABLE_HEADER, WarehouseConstants.LOG_TABLE_NAME);
- headers.set(GREPTIME_LOG_PIPELINE_HEADER, "hertzbeat_otlp_log_v1");
- }
- if (StringUtils.hasText(greptimeProperties.username()) && StringUtils.hasText(greptimeProperties.password())) {
- String credentials = greptimeProperties.username() + ":" + greptimeProperties.password();
- headers.setBasicAuth(Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)));
- }
- return headers;
+ byte[] validContent = validateProtobuf(signal, content);
+ return signalStorage.writeProtobuf(signal, validContent);
}
private byte[] jsonToProtobuf(String signal, byte[] content) {
@@ -213,7 +163,4 @@ private String normalizeSignal(String signal) {
return normalized;
}
- private String endpoint(String base, String path) {
- return StringUtils.trimTrailingCharacter(base, '/') + path;
- }
}
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapter.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/impl/OtlpLogProtocolAdapter.java
similarity index 79%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapter.java
rename to hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/impl/OtlpLogProtocolAdapter.java
index 46978af182e..93db9e7e0b5 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapter.java
+++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/service/impl/OtlpLogProtocolAdapter.java
@@ -15,8 +15,11 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.service.impl;
+package org.apache.hertzbeat.observability.service.impl;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
@@ -29,14 +32,16 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
-import org.apache.hertzbeat.log.notice.LogSseManager;
-import org.apache.hertzbeat.log.service.LogProtocolAdapter;
+import org.apache.hertzbeat.observability.notice.LogSseManager;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
+import java.util.Base64;
import java.util.HashMap;
+import java.util.HexFormat;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Adapter for OpenTelemetry OTLP/HTTP log ingestion.
@@ -46,9 +51,10 @@
*/
@Slf4j
@Service
-public class OtlpLogProtocolAdapter implements LogProtocolAdapter {
+public class OtlpLogProtocolAdapter {
- private static final String PROTOCOL_NAME = "otlp";
+ private static final Set OTLP_HEX_ID_FIELDS = Set.of("traceId", "spanId");
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final CommonDataQueue commonDataQueue;
private final LogSseManager logSseManager;
@@ -58,7 +64,6 @@ public OtlpLogProtocolAdapter(CommonDataQueue commonDataQueue, LogSseManager log
this.logSseManager = logSseManager;
}
- @Override
public void ingest(String content) {
if (content == null || content.isEmpty()) {
log.warn("Received empty OTLP JSON log payload - skip processing.");
@@ -66,7 +71,7 @@ public void ingest(String content) {
}
ExportLogsServiceRequest.Builder builder = ExportLogsServiceRequest.newBuilder();
try {
- JsonFormat.parser().ignoringUnknownFields().merge(content, builder);
+ JsonFormat.parser().ignoringUnknownFields().merge(normalizeOtlpJson(content), builder);
ExportLogsServiceRequest request = builder.build();
processLogsRequest(request, "JSON");
} catch (InvalidProtocolBufferException e) {
@@ -274,8 +279,58 @@ private String bytesToHex(byte[] bytes) {
return hexString.toString();
}
- @Override
- public String supportProtocol() {
- return PROTOCOL_NAME;
+ /**
+ * Accept the hexadecimal IDs emitted by the HertzBeat onboarding probe while preserving
+ * standard base64-encoded OTLP/JSON IDs.
+ */
+ private String normalizeOtlpJson(String content) throws InvalidProtocolBufferException {
+ try {
+ JsonNode root = OBJECT_MAPPER.readTree(content);
+ normalizeOtlpHexEncodedIds(root);
+ return OBJECT_MAPPER.writeValueAsString(root);
+ } catch (Exception exception) {
+ throw new InvalidProtocolBufferException("Failed to normalize OTLP JSON: " + exception.getMessage());
+ }
+ }
+
+ private void normalizeOtlpHexEncodedIds(JsonNode node) {
+ if (node == null) {
+ return;
+ }
+ if (node.isObject()) {
+ ObjectNode objectNode = (ObjectNode) node;
+ objectNode.fieldNames().forEachRemaining(fieldName -> {
+ JsonNode child = objectNode.get(fieldName);
+ if (OTLP_HEX_ID_FIELDS.contains(fieldName) && child != null && child.isTextual()) {
+ String normalized = tryConvertHexToBase64(child.asText());
+ if (normalized != null) {
+ objectNode.put(fieldName, normalized);
+ }
+ } else {
+ normalizeOtlpHexEncodedIds(child);
+ }
+ });
+ return;
+ }
+ if (node.isArray()) {
+ node.forEach(this::normalizeOtlpHexEncodedIds);
+ }
}
-}
\ No newline at end of file
+
+ private String tryConvertHexToBase64(String value) {
+ if (value == null || value.isBlank() || (value.length() & 1) != 0) {
+ return null;
+ }
+ for (int i = 0; i < value.length(); i++) {
+ if (Character.digit(value.charAt(i), 16) < 0) {
+ return null;
+ }
+ }
+ try {
+ return Base64.getEncoder().encodeToString(HexFormat.of().parseHex(value));
+ } catch (IllegalArgumentException exception) {
+ return null;
+ }
+ }
+
+}
diff --git a/hertzbeat-observability/src/main/resources/META-INF/spring.factories b/hertzbeat-observability/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000000..076463ce82b
--- /dev/null
+++ b/hertzbeat-observability/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,17 @@
+# 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.
+
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.apache.hertzbeat.observability.config.ObservabilityAutoConfiguration
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/config/OtlpGrpcServerConfigTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/config/OtlpGrpcServerConfigTest.java
similarity index 74%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/config/OtlpGrpcServerConfigTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/config/OtlpGrpcServerConfigTest.java
index 41245fe3914..d762cd37873 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/config/OtlpGrpcServerConfigTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/config/OtlpGrpcServerConfigTest.java
@@ -15,13 +15,14 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.config;
+package org.apache.hertzbeat.observability.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.grpc.ManagedChannel;
@@ -33,15 +34,19 @@
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import io.grpc.stub.MetadataUtils;
+import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
+import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse;
+import io.opentelemetry.proto.collector.logs.v1.LogsServiceGrpc;
import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse;
import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.apache.hertzbeat.common.security.OtlpAccessTokenValidator;
-import org.apache.hertzbeat.log.service.OtlpSignalForwarder;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard;
-import org.apache.hertzbeat.log.service.SignalQueryRejectedException;
+import org.apache.hertzbeat.observability.service.OtlpLogIngestionService;
+import org.apache.hertzbeat.observability.service.OtlpSignalForwarder;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
+import org.apache.hertzbeat.observability.service.SignalQueryRejectedException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -50,10 +55,12 @@ class OtlpGrpcServerConfigTest {
private Server server;
private ManagedChannel channel;
+ private OtlpLogIngestionService logIngestionService;
@BeforeEach
void setUp() throws Exception {
OtlpSignalForwarder forwarder = mock(OtlpSignalForwarder.class);
+ logIngestionService = mock(OtlpLogIngestionService.class);
SignalWorkloadGuard guard = mock(SignalWorkloadGuard.class);
OtlpAccessTokenValidator validator = token -> "probe-token".equals(token) ? null : "Invalid token";
when(guard.execute(eq(SignalWorkloadGuard.Workload.OTLP_WRITE), any())).thenAnswer(invocation -> {
@@ -61,10 +68,14 @@ void setUp() throws Exception {
return action.get();
});
when(forwarder.forwardProtobuf(eq("metrics"), any())).thenReturn(new byte[0]);
+ when(logIngestionService.ingestProtobuf(any())).thenReturn(new byte[0]);
server = NettyServerBuilder.forPort(0)
.addService(ServerInterceptors.intercept(
new OtlpGrpcServerConfig.MetricsService(forwarder, guard),
new OtlpGrpcServerConfig.BearerTokenInterceptor(validator)))
+ .addService(ServerInterceptors.intercept(
+ new OtlpGrpcServerConfig.LogsService(logIngestionService, guard),
+ new OtlpGrpcServerConfig.BearerTokenInterceptor(validator)))
.build().start();
channel = NettyChannelBuilder.forAddress("127.0.0.1", server.getPort()).usePlaintext().build();
}
@@ -86,6 +97,18 @@ void shouldAcceptAuthorizedOtlpGrpcMetricsRequest() {
assertThat(response).isEqualTo(ExportMetricsServiceResponse.getDefaultInstance());
}
+ @Test
+ void shouldRouteAuthorizedOtlpGrpcLogsThroughLogIngestionService() {
+ Metadata headers = new Metadata();
+ headers.put(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER), "Bearer probe-token");
+ ExportLogsServiceResponse response = LogsServiceGrpc.newBlockingStub(channel)
+ .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(headers))
+ .export(ExportLogsServiceRequest.getDefaultInstance());
+
+ assertThat(response).isEqualTo(ExportLogsServiceResponse.getDefaultInstance());
+ verify(logIngestionService).ingestProtobuf(any());
+ }
+
@Test
void shouldRejectMissingOtlpGrpcBearerToken() {
assertThatThrownBy(() -> MetricsServiceGrpc.newBlockingStub(channel)
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LegacyOtlpLogRouteControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LegacyOtlpLogRouteControllerTest.java
new file mode 100644
index 00000000000..bdf26ca76e0
--- /dev/null
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LegacyOtlpLogRouteControllerTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.observability.controller;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.apache.hertzbeat.observability.service.OtlpLogIngestionService;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+/** Deprecated 1.8.x OTLP log route alias contract tests. */
+@ExtendWith(MockitoExtension.class)
+class LegacyOtlpLogRouteControllerTest {
+
+ @Mock
+ private OtlpLogIngestionService logIngestionService;
+
+ private MockMvc mockMvc;
+
+ @BeforeEach
+ void setUp() {
+ mockMvc = MockMvcBuilders.standaloneSetup(
+ new LegacyOtlpLogRouteController(logIngestionService, new SignalWorkloadGuard())).build();
+ }
+
+ @Test
+ void legacyOtlpRouteShouldForwardToTheCanonicalLogFanOutWithDeprecationHeaders() throws Exception {
+ when(logIngestionService.ingestHttp(any(), any()))
+ .thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body("{}".getBytes()));
+
+ mockMvc.perform(MockMvcRequestBuilders.post("/api/logs/otlp/v1/logs")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{}"))
+ .andExpect(status().isOk())
+ .andExpect(header().string("Deprecation", "true"))
+ .andExpect(header().string("Link", "; rel=\"successor-version\""))
+ .andExpect(content().string("{}"));
+
+ verify(logIngestionService).ingestHttp(any(), any(HttpHeaders.class));
+ }
+
+ @Test
+ void legacyIngestOtlpRouteShouldForwardToTheCanonicalLogFanOut() throws Exception {
+ when(logIngestionService.ingestHttp(any(), any()))
+ .thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body("{}".getBytes()));
+
+ mockMvc.perform(MockMvcRequestBuilders.post("/api/logs/ingest/OTLP")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{}"))
+ .andExpect(status().isOk())
+ .andExpect(header().string("Deprecation", "true"));
+
+ verify(logIngestionService).ingestHttp(any(), any(HttpHeaders.class));
+ }
+
+ @Test
+ void legacyIngestRouteShouldRejectNonOtlpProtocolsWithoutTouchingTheFanOut() throws Exception {
+ mockMvc.perform(MockMvcRequestBuilders.post("/api/logs/ingest/vector")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(header().string("Deprecation", "true"));
+
+ verifyNoInteractions(logIngestionService);
+ }
+
+ @Test
+ void legacyRouteShouldPreserveCanonicalErrorStatus() throws Exception {
+ when(logIngestionService.ingestHttp(any(), any()))
+ .thenThrow(new IllegalArgumentException("Malformed OTLP logs JSON payload"));
+
+ mockMvc.perform(MockMvcRequestBuilders.post("/api/logs/otlp/v1/logs")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{"))
+ .andExpect(status().isBadRequest())
+ .andExpect(header().string("Deprecation", "true"))
+ .andExpect(content().string("Malformed OTLP logs JSON payload"));
+ }
+}
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogManagerControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LogManagerControllerTest.java
similarity index 94%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogManagerControllerTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LogManagerControllerTest.java
index 8bd4939e0b6..29e73e0db37 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogManagerControllerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LogManagerControllerTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.when;
@@ -61,7 +61,7 @@ void testBatchDeleteLogsSuccess() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders
- .delete("/api/logs")
+ .delete("/api/observability/logs")
.param("timeUnixNanos", "1734005477630000000", "1734005477640000000")
)
.andDo(print())
@@ -77,7 +77,7 @@ void testBatchDeleteLogsFailure() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders
- .delete("/api/logs")
+ .delete("/api/observability/logs")
.param("timeUnixNanos", "1734005477630000000", "1734005477640000000")
)
.andExpect(status().isOk())
@@ -91,7 +91,7 @@ void testBatchDeleteLogsWithSingleTimestamp() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders
- .delete("/api/logs")
+ .delete("/api/observability/logs")
.param("timeUnixNanos", "1734005477630000000")
)
.andExpect(status().isOk())
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogQueryControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LogQueryControllerTest.java
similarity index 91%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogQueryControllerTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LogQueryControllerTest.java
index 8dd39de96d6..902ca5142ef 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogQueryControllerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LogQueryControllerTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -32,7 +32,7 @@
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.log.LogEntry;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -69,8 +69,8 @@ void setUp() {
void shouldReturnFriendlyFailureWhenLogQueryUnsupported() throws Exception {
when(historyDataReader.supportsLogQuery()).thenReturn(false);
- for (String path : List.of("/api/logs/list", "/api/logs/stats/overview",
- "/api/logs/stats/trace-coverage", "/api/logs/stats/trend")) {
+ for (String path : List.of("/api/observability/logs", "/api/observability/logs/overview",
+ "/api/observability/logs/trace-coverage", "/api/observability/logs/trend")) {
mockMvc.perform(MockMvcRequestBuilders.get(path))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE))
@@ -108,7 +108,7 @@ void testListLogsWithAllFilters() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders
- .get("/api/logs/list")
+ .get("/api/observability/logs")
.param("start", "1734005477000")
.param("end", "1734005478000")
.param("traceId", "trace123")
@@ -144,7 +144,7 @@ void testListLogsWithoutFilters() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders
- .get("/api/logs/list")
+ .get("/api/observability/logs")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
@@ -157,7 +157,7 @@ void shouldBoundLogPaginationBeforeReadingStorage() throws Exception {
when(historyDataReader.countObservabilityLogs(any())).thenReturn(0L);
when(historyDataReader.queryObservabilityLogs(any(), any(), any())).thenReturn(List.of());
- mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/list")
+ mockMvc.perform(MockMvcRequestBuilders.get("/api/observability/logs")
.param("pageIndex", "-1")
.param("pageSize", "1000"))
.andExpect(status().isOk())
@@ -172,7 +172,7 @@ void shouldReturnBadRequestForInvalidResourceExpression() throws Exception {
when(historyDataReader.countObservabilityLogs(any()))
.thenThrow(new IllegalArgumentException("Invalid resource filter expression"));
- mockMvc.perform(MockMvcRequestBuilders.get("/api/logs/list")
+ mockMvc.perform(MockMvcRequestBuilders.get("/api/observability/logs")
.param("resource", "bad-expression"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE));
@@ -186,7 +186,7 @@ void testOverviewStatsWithMixedSeverityLogs() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders
- .get("/api/logs/stats/overview")
+ .get("/api/observability/logs/overview")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
@@ -205,7 +205,7 @@ void testOverviewStatsWithTimeRange() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders
- .get("/api/logs/stats/overview")
+ .get("/api/observability/logs/overview")
.param("start", "1734005477000")
.param("end", "1734005478000")
)
@@ -221,7 +221,7 @@ void testTraceCoverageStats() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders
- .get("/api/logs/stats/trace-coverage")
+ .get("/api/observability/logs/trace-coverage")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
@@ -238,7 +238,7 @@ void testTrendStats() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders
- .get("/api/logs/stats/trend")
+ .get("/api/observability/logs/trend")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
@@ -251,7 +251,7 @@ void testTrendStatsWithNullTimestamp() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders
- .get("/api/logs/stats/trend")
+ .get("/api/observability/logs/trend")
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogSseControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LogSseControllerTest.java
similarity index 92%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogSseControllerTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LogSseControllerTest.java
index 5528ec4f92a..302e0e925ac 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogSseControllerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/LogSseControllerTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -24,8 +24,8 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-import org.apache.hertzbeat.log.notice.LogSseFilterCriteria;
-import org.apache.hertzbeat.log.notice.LogSseManager;
+import org.apache.hertzbeat.observability.notice.LogSseFilterCriteria;
+import org.apache.hertzbeat.observability.notice.LogSseManager;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -64,7 +64,7 @@ void setUp() {
@Test
void testSubscribeWithoutFilters() throws Exception {
// When: A request is made to the subscribe endpoint without any parameters
- mockMvc.perform(get("/api/logs/sse/subscribe")
+ mockMvc.perform(get("/api/observability/logs/stream")
.accept(MediaType.TEXT_EVENT_STREAM_VALUE))
.andExpect(status().isOk());
@@ -86,7 +86,7 @@ void testSubscribeCommitsInitialEvent() throws Exception {
.standaloneSetup(new LogSseController(realEmitterManager))
.build();
- MvcResult result = realMockMvc.perform(get("/api/logs/sse/subscribe")
+ MvcResult result = realMockMvc.perform(get("/api/observability/logs/stream")
.accept(MediaType.TEXT_EVENT_STREAM_VALUE))
.andExpect(status().isOk())
.andExpect(request().asyncStarted())
@@ -109,7 +109,7 @@ void testSubscribeWithMultipleFilters() throws Exception {
String spanId = "abcdef1234567890";
// When: A request is made with all filter parameters
- mockMvc.perform(get("/api/logs/sse/subscribe")
+ mockMvc.perform(get("/api/observability/logs/stream")
.param("severityText", severityText)
.param("severityNumber", severityNumber)
.param("traceId", traceId)
@@ -130,7 +130,7 @@ void testSubscribeWithMultipleFilters() throws Exception {
@Test
void testSubscribeWithInvalidSeverityNumber() throws Exception {
// When: A request is made with a non-integer value for severityNumber
- mockMvc.perform(get("/api/logs/sse/subscribe")
+ mockMvc.perform(get("/api/observability/logs/stream")
.param("severityNumber", "not-a-number")
.accept(MediaType.TEXT_EVENT_STREAM_VALUE))
.andExpect(status().is4xxClientError());
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/ObservabilityCapabilityControllerTest.java
similarity index 96%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityControllerTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/ObservabilityCapabilityControllerTest.java
index e406213d116..dd6b54cb85f 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityControllerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/ObservabilityCapabilityControllerTest.java
@@ -15,14 +15,14 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.apache.hertzbeat.common.constants.CommonConstants;
-import org.apache.hertzbeat.log.service.ThreeSignalQueryService;
+import org.apache.hertzbeat.warehouse.service.ThreeSignalQueryService;
import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/OtlpLogControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/OtlpLogControllerTest.java
new file mode 100644
index 00000000000..2d6aec81260
--- /dev/null
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/OtlpLogControllerTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.observability.controller;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.apache.hertzbeat.observability.service.OtlpLogIngestionService;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+/** OTLP/HTTP log route contract tests. */
+@ExtendWith(MockitoExtension.class)
+class OtlpLogControllerTest {
+
+ @Mock
+ private OtlpLogIngestionService logIngestionService;
+
+ private MockMvc mockMvc;
+
+ @BeforeEach
+ void setUp() {
+ mockMvc = MockMvcBuilders.standaloneSetup(
+ new OtlpLogController(logIngestionService, new SignalWorkloadGuard())).build();
+ }
+
+ @Test
+ void shouldRouteLogsToTheLogFanOut() throws Exception {
+ when(logIngestionService.ingestHttp(any(), any()))
+ .thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body("{}".getBytes()));
+
+ mockMvc.perform(MockMvcRequestBuilders.post("/api/otlp/v1/logs")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{}"))
+ .andExpect(status().isOk());
+
+ verify(logIngestionService).ingestHttp(any(), any(HttpHeaders.class));
+ }
+
+ @Test
+ void shouldReturnBadRequestForMalformedOtlpPayload() throws Exception {
+ when(logIngestionService.ingestHttp(any(), any()))
+ .thenThrow(new IllegalArgumentException("Malformed OTLP logs JSON payload"));
+
+ mockMvc.perform(MockMvcRequestBuilders.post("/api/otlp/v1/logs")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{"))
+ .andExpect(status().isBadRequest())
+ .andExpect(content().string("Malformed OTLP logs JSON payload"));
+ }
+}
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/OtlpLogIngestionOwnershipTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/OtlpLogIngestionOwnershipTest.java
new file mode 100644
index 00000000000..1039b463127
--- /dev/null
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/OtlpLogIngestionOwnershipTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.observability.controller;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.jupiter.api.Test;
+
+class OtlpLogIngestionOwnershipTest {
+
+ @Test
+ void httpAndGrpcLogsShouldUseTheLogFanOutInsteadOfTheRawGreptimeForwarder() throws Exception {
+ String httpController = Files.readString(Path.of(
+ "src/main/java/org/apache/hertzbeat/observability/controller/OtlpLogController.java"));
+ String grpcConfig = Files.readString(Path.of(
+ "src/main/java/org/apache/hertzbeat/observability/config/OtlpGrpcServerConfig.java"));
+
+ assertTrue(httpController.contains("OtlpLogIngestionService"));
+ assertTrue(httpController.contains("logIngestionService.ingestHttp(content, headers)"));
+ assertFalse(httpController.contains("forward(\"logs\""));
+
+ assertTrue(grpcConfig.contains("OtlpLogIngestionService"));
+ assertTrue(grpcConfig.contains("logIngestionService.ingestProtobuf(request.toByteArray())"));
+ assertFalse(grpcConfig.contains("forwardProtobuf(\"logs\""));
+ }
+}
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/OtlpSignalControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/OtlpSignalControllerTest.java
similarity index 88%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/OtlpSignalControllerTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/OtlpSignalControllerTest.java
index a3e48a14e04..e9936c9212c 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/OtlpSignalControllerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/OtlpSignalControllerTest.java
@@ -15,17 +15,18 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-import org.apache.hertzbeat.log.service.OtlpSignalForwarder;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard;
+import org.apache.hertzbeat.observability.service.OtlpSignalForwarder;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -54,17 +55,19 @@ void setUp() {
}
@Test
- void shouldRouteAllThreeSignals() throws Exception {
+ void shouldRouteGreptimeSignals() throws Exception {
when(signalForwarder.forwardHttp(any(), any(), any()))
.thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body("{}".getBytes()));
- for (String signal : new String[] {"metrics", "logs", "traces"}) {
+ for (String signal : new String[] {"metrics", "traces"}) {
mockMvc.perform(MockMvcRequestBuilders.post("/api/otlp/v1/" + signal)
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isOk());
verify(signalForwarder).forwardHttp(eq(signal), any(), any(HttpHeaders.class));
}
+
+ verifyNoMoreInteractions(signalForwarder);
}
@Test
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/SignalWorkloadExceptionHandlerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/SignalWorkloadExceptionHandlerTest.java
similarity index 94%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/SignalWorkloadExceptionHandlerTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/SignalWorkloadExceptionHandlerTest.java
index 9e3a32fa6da..cd4c49bcf4a 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/SignalWorkloadExceptionHandlerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/SignalWorkloadExceptionHandlerTest.java
@@ -15,13 +15,13 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.apache.hertzbeat.common.entity.dto.Message;
-import org.apache.hertzbeat.log.service.SignalQueryRejectedException;
+import org.apache.hertzbeat.observability.service.SignalQueryRejectedException;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/ThreeSignalQueryControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/ThreeSignalQueryControllerTest.java
similarity index 91%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/ThreeSignalQueryControllerTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/ThreeSignalQueryControllerTest.java
index a98c9fdd2e3..260e6714741 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/ThreeSignalQueryControllerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/controller/ThreeSignalQueryControllerTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.controller;
+package org.apache.hertzbeat.observability.controller;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -30,8 +30,8 @@
import org.apache.hertzbeat.common.entity.dto.observability.TraceListItem;
import org.apache.hertzbeat.common.entity.dto.observability.TraceOverview;
import org.apache.hertzbeat.common.entity.dto.observability.TraceSpanNode;
-import org.apache.hertzbeat.log.service.ThreeSignalQueryService;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard;
+import org.apache.hertzbeat.warehouse.service.ThreeSignalQueryService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -62,7 +62,7 @@ void shouldQueryMetricsWithoutEntityContext() throws Exception {
"prod", null, null, null, 60, null))
.thenReturn(new OtlpMetricsConsole("http_server_duration", 1000L, 2000L, 60, List.of()));
- mockMvc.perform(MockMvcRequestBuilders.get("/api/ingestion/otlp/metrics/console")
+ mockMvc.perform(MockMvcRequestBuilders.get("/api/observability/metrics/query")
.param("query", "http_server_duration")
.param("start", "1000")
.param("end", "2000")
@@ -80,7 +80,7 @@ void shouldExposeMetricInventory() throws Exception {
when(queryService.metricInventory(1000L, 2000L, "checkout", "payments", "prod", 50))
.thenReturn(new OtlpMetricsInventory(List.of("http_server_duration")));
- mockMvc.perform(MockMvcRequestBuilders.get("/api/ingestion/otlp/metrics/inventory")
+ mockMvc.perform(MockMvcRequestBuilders.get("/api/observability/metrics/inventory")
.param("start", "1000")
.param("end", "2000")
.param("serviceName", "checkout")
@@ -102,7 +102,7 @@ void shouldQueryTraceListAndDetail() throws Exception {
"SERVER", "failed", 10_000_000L, 1000L, Map.of(), Map.of(), List.of());
when(queryService.traceDetail("trace-1")).thenReturn(new TraceDetail(item, List.of(span)));
- mockMvc.perform(MockMvcRequestBuilders.get("/api/traces/list")
+ mockMvc.perform(MockMvcRequestBuilders.get("/api/observability/traces")
.param("start", "1000")
.param("end", "2000")
.param("traceId", "trace-1")
@@ -116,7 +116,7 @@ void shouldQueryTraceListAndDetail() throws Exception {
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.content[0].traceId").value("trace-1"));
- mockMvc.perform(MockMvcRequestBuilders.get("/api/traces/trace-1"))
+ mockMvc.perform(MockMvcRequestBuilders.get("/api/observability/traces/trace-1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.spans[0].spanId").value("span-1"));
}
@@ -127,7 +127,7 @@ void shouldExposeTraceOverview() throws Exception {
"GET /cart", 1L, 100L))
.thenReturn(new TraceOverview(4, 1, 0.25, 12.5, 24.0));
- mockMvc.perform(MockMvcRequestBuilders.get("/api/traces/stats/overview")
+ mockMvc.perform(MockMvcRequestBuilders.get("/api/observability/traces/overview")
.param("start", "1000")
.param("end", "2000")
.param("traceId", "trace-1")
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteriaTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/notice/LogSseFilterCriteriaTest.java
similarity index 99%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteriaTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/notice/LogSseFilterCriteriaTest.java
index b06284d9bae..1845e6bc5b1 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseFilterCriteriaTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/notice/LogSseFilterCriteriaTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.notice;
+package org.apache.hertzbeat.observability.notice;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.junit.jupiter.api.BeforeEach;
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseManagerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/notice/LogSseManagerTest.java
similarity index 99%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseManagerTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/notice/LogSseManagerTest.java
index 231e8af8772..2616507a307 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/notice/LogSseManagerTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/notice/LogSseManagerTest.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.hertzbeat.log.notice;
+package org.apache.hertzbeat.observability.notice;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.junit.jupiter.api.AfterEach;
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/SignalWorkloadGuardTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/SignalWorkloadGuardTest.java
similarity index 92%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/SignalWorkloadGuardTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/SignalWorkloadGuardTest.java
index d8c92d7625b..d0c496af05d 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/SignalWorkloadGuardTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/SignalWorkloadGuardTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.service;
+package org.apache.hertzbeat.observability.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -25,8 +25,8 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard.Limits;
-import org.apache.hertzbeat.log.service.SignalWorkloadGuard.Workload;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard.Limits;
+import org.apache.hertzbeat.observability.service.SignalWorkloadGuard.Workload;
import org.junit.jupiter.api.Test;
/** Tests bounded signal workload isolation and recovery. */
diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/impl/DefaultOtlpLogIngestionServiceTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/impl/DefaultOtlpLogIngestionServiceTest.java
new file mode 100644
index 00000000000..791bc616560
--- /dev/null
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/impl/DefaultOtlpLogIngestionServiceTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.observability.service.impl;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.verify;
+
+import io.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.GZIPOutputStream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+
+@ExtendWith(MockitoExtension.class)
+class DefaultOtlpLogIngestionServiceTest {
+
+ @Mock
+ private OtlpLogProtocolAdapter protocolAdapter;
+
+ private DefaultOtlpLogIngestionService service;
+
+ @BeforeEach
+ void setUp() {
+ service = new DefaultOtlpLogIngestionService(protocolAdapter);
+ }
+
+ @Test
+ void shouldIngestJsonAndReturnJsonOtlpResponse() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+
+ var response = service.ingestHttp("{}".getBytes(StandardCharsets.UTF_8), headers);
+
+ verify(protocolAdapter).ingest("{}");
+ assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
+ assertThat(response.getBody()).isEqualTo("{}".getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Test
+ void shouldIngestProtobufAndReturnProtobufOtlpResponse() {
+ byte[] request = ExportLogsServiceRequest.getDefaultInstance().toByteArray();
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.parseMediaType("application/x-protobuf"));
+
+ var response = service.ingestHttp(request, headers);
+
+ verify(protocolAdapter).ingestBinary(request);
+ assertThat(response.getHeaders().getContentType())
+ .isEqualTo(MediaType.parseMediaType("application/x-protobuf"));
+ assertThat(response.getBody()).isEmpty();
+ }
+
+ @Test
+ void shouldDecompressCommaSeparatedGzipContentEncoding() throws Exception {
+ byte[] request = ExportLogsServiceRequest.getDefaultInstance().toByteArray();
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.parseMediaType("application/x-protobuf"));
+ headers.add(HttpHeaders.CONTENT_ENCODING, "identity, gzip");
+
+ service.ingestHttp(gzip(request), headers);
+
+ verify(protocolAdapter).ingestBinary(request);
+ }
+
+ @Test
+ void shouldRejectMalformedGzipBeforeFanOut() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.parseMediaType("application/x-protobuf"));
+ headers.set(HttpHeaders.CONTENT_ENCODING, "gzip");
+
+ assertThatThrownBy(() -> service.ingestHttp(new byte[] {1, 2, 3}, headers))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Malformed gzip OTLP log payload");
+ }
+
+ private byte[] gzip(byte[] content) throws Exception {
+ try (ByteArrayOutputStream output = new ByteArrayOutputStream();
+ GZIPOutputStream gzip = new GZIPOutputStream(output)) {
+ gzip.write(content);
+ gzip.finish();
+ return output.toByteArray();
+ }
+ }
+}
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/impl/GreptimeOtlpSignalForwarderTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/impl/GreptimeOtlpSignalForwarderTest.java
similarity index 63%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/impl/GreptimeOtlpSignalForwarderTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/impl/GreptimeOtlpSignalForwarderTest.java
index 07b4cd18151..c5a03ed6ff9 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/impl/GreptimeOtlpSignalForwarderTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/impl/GreptimeOtlpSignalForwarderTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.service.impl;
+package org.apache.hertzbeat.observability.service.impl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -28,33 +28,27 @@
import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest;
import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
import java.nio.charset.StandardCharsets;
-import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
+import org.apache.hertzbeat.warehouse.service.OtlpSignalStorage;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.client.RestTemplate;
/** Greptime native OTLP forwarding tests. */
@ExtendWith(MockitoExtension.class)
class GreptimeOtlpSignalForwarderTest {
@Mock
- private RestTemplate restTemplate;
+ private OtlpSignalStorage signalStorage;
@Test
- void shouldConvertJsonAndForwardMetricsWithResourcePromotion() {
- when(restTemplate.exchange(eq("http://127.0.0.1:4000/v1/otlp/v1/metrics"), eq(HttpMethod.POST),
- any(HttpEntity.class), eq(byte[].class))).thenReturn(ResponseEntity.ok(new byte[0]));
- GreptimeOtlpSignalForwarder forwarder = new GreptimeOtlpSignalForwarder(
- new GreptimeProperties(true, "127.0.0.1:4001", "http://127.0.0.1:4000",
- "public", "greptime", "secret"), restTemplate);
+ void shouldConvertJsonBeforeWritingMetrics() {
+ when(signalStorage.writeProtobuf(eq("metrics"), any())).thenReturn(new byte[0]);
+ GreptimeOtlpSignalForwarder forwarder = new GreptimeOtlpSignalForwarder(signalStorage);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
@@ -62,21 +56,15 @@ void shouldConvertJsonAndForwardMetricsWithResourcePromotion() {
headers);
assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
- ArgumentCaptor> request = ArgumentCaptor.forClass(HttpEntity.class);
- verify(restTemplate).exchange(eq("http://127.0.0.1:4000/v1/otlp/v1/metrics"), eq(HttpMethod.POST),
- request.capture(), eq(byte[].class));
- assertThat(request.getValue().getHeaders().getFirst("X-Greptime-OTLP-Metric-Promote-Resource-Attrs"))
- .contains("service.name", "deployment.environment.name");
- assertThat(request.getValue().getBody()).isEqualTo(ExportMetricsServiceRequest.getDefaultInstance().toByteArray());
+ ArgumentCaptor content = ArgumentCaptor.forClass(byte[].class);
+ verify(signalStorage).writeProtobuf(eq("metrics"), content.capture());
+ assertThat(content.getValue()).isEqualTo(ExportMetricsServiceRequest.getDefaultInstance().toByteArray());
}
@Test
void shouldAcceptStandardHexTraceIdsInOtlpJson() throws Exception {
- when(restTemplate.exchange(eq("http://127.0.0.1:4000/v1/otlp/v1/traces"), eq(HttpMethod.POST),
- any(HttpEntity.class), eq(byte[].class))).thenReturn(ResponseEntity.ok(new byte[0]));
- GreptimeOtlpSignalForwarder forwarder = new GreptimeOtlpSignalForwarder(
- new GreptimeProperties(true, "127.0.0.1:4001", "http://127.0.0.1:4000",
- "public", "greptime", "secret"), restTemplate);
+ when(signalStorage.writeProtobuf(eq("traces"), any())).thenReturn(new byte[0]);
+ GreptimeOtlpSignalForwarder forwarder = new GreptimeOtlpSignalForwarder(signalStorage);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String json = "{\"resourceSpans\":[{\"scopeSpans\":[{\"spans\":[{"
@@ -86,10 +74,9 @@ void shouldAcceptStandardHexTraceIdsInOtlpJson() throws Exception {
forwarder.forwardHttp("traces", json.getBytes(StandardCharsets.UTF_8), headers);
- ArgumentCaptor> request = ArgumentCaptor.forClass(HttpEntity.class);
- verify(restTemplate).exchange(eq("http://127.0.0.1:4000/v1/otlp/v1/traces"), eq(HttpMethod.POST),
- request.capture(), eq(byte[].class));
- ExportTraceServiceRequest parsed = ExportTraceServiceRequest.parseFrom(request.getValue().getBody());
+ ArgumentCaptor content = ArgumentCaptor.forClass(byte[].class);
+ verify(signalStorage).writeProtobuf(eq("traces"), content.capture());
+ ExportTraceServiceRequest parsed = ExportTraceServiceRequest.parseFrom(content.getValue());
assertThat(parsed.getResourceSpans(0).getScopeSpans(0).getSpans(0).getTraceId().toByteArray())
.containsExactly(java.util.HexFormat.of().parseHex("0123456789abcdef0123456789abcdef"));
assertThat(parsed.getResourceSpans(0).getScopeSpans(0).getSpans(0).getParentSpanId().toByteArray())
@@ -98,29 +85,25 @@ void shouldAcceptStandardHexTraceIdsInOtlpJson() throws Exception {
@Test
void shouldRejectMalformedJsonBeforeCallingGreptime() {
- GreptimeOtlpSignalForwarder forwarder = new GreptimeOtlpSignalForwarder(
- new GreptimeProperties(true, "127.0.0.1:4001", "http://127.0.0.1:4000",
- "public", "greptime", "secret"), restTemplate);
+ GreptimeOtlpSignalForwarder forwarder = new GreptimeOtlpSignalForwarder(signalStorage);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
assertThatThrownBy(() -> forwarder.forwardHttp("metrics", "{".getBytes(StandardCharsets.UTF_8), headers))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Malformed OTLP JSON payload");
- verifyNoInteractions(restTemplate);
+ verifyNoInteractions(signalStorage);
}
@Test
void shouldRejectMalformedProtobufBeforeCallingGreptime() {
- GreptimeOtlpSignalForwarder forwarder = new GreptimeOtlpSignalForwarder(
- new GreptimeProperties(true, "127.0.0.1:4001", "http://127.0.0.1:4000",
- "public", "greptime", "secret"), restTemplate);
+ GreptimeOtlpSignalForwarder forwarder = new GreptimeOtlpSignalForwarder(signalStorage);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/x-protobuf"));
assertThatThrownBy(() -> forwarder.forwardHttp("traces", new byte[] {(byte) 0xff}, headers))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Malformed OTLP traces protobuf payload");
- verifyNoInteractions(restTemplate);
+ verifyNoInteractions(signalStorage);
}
}
diff --git a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapterTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/impl/OtlpLogProtocolAdapterTest.java
similarity index 94%
rename from hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapterTest.java
rename to hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/impl/OtlpLogProtocolAdapterTest.java
index 0a570155bfd..bcd9b150e14 100644
--- a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/service/impl/OtlpLogProtocolAdapterTest.java
+++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/service/impl/OtlpLogProtocolAdapterTest.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.service.impl;
+package org.apache.hertzbeat.observability.service.impl;
import com.google.protobuf.ByteString;
import com.google.protobuf.util.JsonFormat;
@@ -29,7 +29,7 @@
import io.opentelemetry.proto.common.v1.InstrumentationScope;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
-import org.apache.hertzbeat.log.notice.LogSseManager;
+import org.apache.hertzbeat.observability.notice.LogSseManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -211,6 +211,31 @@ void testIngestWithTraceAndSpanIds() throws Exception {
assertEquals(1, capturedEntry.getTraceFlags());
}
+ @Test
+ void testIngestWithHexEncodedTraceAndSpanIds() {
+ String otlpPayload = """
+ {
+ "resourceLogs": [{
+ "scopeLogs": [{
+ "logRecords": [{
+ "body": {"stringValue": "hex id probe"},
+ "traceId": "1234567890abcdef1234567890abcdef",
+ "spanId": "1234567890abcdef"
+ }]
+ }]
+ }]
+ }
+ """;
+
+ adapter.ingest(otlpPayload);
+
+ ArgumentCaptor> listCaptor = ArgumentCaptor.forClass(List.class);
+ verify(commonDataQueue).sendLogEntryToStorageBatch(listCaptor.capture());
+ LogEntry capturedEntry = listCaptor.getValue().getFirst();
+ assertEquals("1234567890abcdef1234567890abcdef", capturedEntry.getTraceId());
+ assertEquals("1234567890abcdef", capturedEntry.getSpanId());
+ }
+
@Test
void testIngestWithInvalidJsonContent() {
String invalidJson = "{ invalid json content }";
diff --git a/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/config/OpenTelemetryAutoConfiguration.java b/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/config/OpenTelemetryAutoConfiguration.java
new file mode 100644
index 00000000000..61f145a7ff2
--- /dev/null
+++ b/hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/config/OpenTelemetryAutoConfiguration.java
@@ -0,0 +1,27 @@
+/*
+ * 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.config;
+
+import org.springframework.context.annotation.ComponentScan;
+
+/**
+ * HertzBeat self-telemetry auto configuration.
+ */
+@ComponentScan(basePackageClasses = OpenTelemetryConfig.class)
+public class OpenTelemetryAutoConfiguration {
+}
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 134de326215..1cebd09e56c 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
@@ -51,8 +51,8 @@ public class OpenTelemetryConfig {
private static final String HERTZBEAT_SERVICE_NAME = "HertzBeat";
private static final String DEFAULT_GREPTIME_DB_NAME = "public";
- private static final String DEFAULT_LOGS_TABLE_NAME = "hzb_logs";
- private static final String DEFAULT_TRACES_TABLE_NAME = "hzb_traces";
+ private static final String DEFAULT_LOGS_TABLE_NAME = "hzb_internal_logs";
+ private static final String DEFAULT_TRACES_TABLE_NAME = "hzb_internal_traces";
private static final String GREPTIME_DB_NAME_HEADER = "X-Greptime-DB-Name";
private static final String GREPTIME_LOG_TABLE_NAME_HEADER = "X-Greptime-Log-Table-Name";
private static final String GREPTIME_TRACE_TABLE_NAME_HEADER = "X-Greptime-Trace-Table-Name";
@@ -144,4 +144,4 @@ public AutoConfigurationCustomizerProvider greptimeOtelCustomizer(GreptimeProper
return sdkLoggerProviderBuilder.addLogRecordProcessor(batchLogProcessor);
});
}
-}
\ No newline at end of file
+}
diff --git a/hertzbeat-otel/src/main/resources/META-INF/spring.factories b/hertzbeat-otel/src/main/resources/META-INF/spring.factories
index cb9d601c41b..246f2ad72ad 100644
--- a/hertzbeat-otel/src/main/resources/META-INF/spring.factories
+++ b/hertzbeat-otel/src/main/resources/META-INF/spring.factories
@@ -14,4 +14,4 @@
# limitations under the License.
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-org.apache.hertzbeat.otel.config.LogAutoConfiguration
\ No newline at end of file
+org.apache.hertzbeat.otel.config.OpenTelemetryAutoConfiguration
diff --git a/hertzbeat-startup/src/main/resources/sureness.yml b/hertzbeat-startup/src/main/resources/sureness.yml
index 5aa2f8a26e5..f602231b2c9 100644
--- a/hertzbeat-startup/src/main/resources/sureness.yml
+++ b/hertzbeat-startup/src/main/resources/sureness.yml
@@ -88,8 +88,6 @@ resourceRole:
# route forwards a raw promql expression straight to the time series database
- /api/warehouse/**===get===[admin,user,guest]
- /api/warehouse/query===post===[admin]
- - /api/logs/otlp/**===post===[admin,user]
- - /api/logs===delete===[admin]
- /api/v2/alerts===post===[admin,user]
- /api/status/page/**===get===[admin,user,guest]
- /api/status/page/**===post===[admin,user]
@@ -111,12 +109,12 @@ resourceRole:
- /api/ai/**===post===[admin]
- /api/ai/**===put===[admin]
- /api/ai/**===delete===[admin]
- - /api/logs/sse/**===get===[admin,user,guest]
+ - /api/otlp/v1/**===post===[admin,user]
+ # deprecated 1.8.x OTLP log aliases, forwarded to /api/otlp/v1/logs, removed in 2.0
+ - /api/logs/otlp/**===post===[admin,user]
- /api/logs/ingest/**===post===[admin,user]
- - /api/otlp/**===post===[admin,user]
- - /api/ingestion/otlp/**===get===[admin,user,guest]
- - /api/logs/**===get===[admin,user,guest]
- - /api/traces/**===get===[admin,user,guest]
+ - /api/observability/logs===delete===[admin]
+ - /api/observability/**===get===[admin,user,guest]
- /api/account/token===get===[admin]
- /api/account/token/**===post===[admin]
- /api/account/token/**===delete===[admin]
@@ -138,7 +136,6 @@ excludedResource:
- /api/account/auth/**===*
- /api/i18n/**===get
- /api/apps/hierarchy===get
- - /api/observability/capability===get
- /api/push/**===*
- /api/status/page/public/**===*
- /api/manager/sse/**===*
diff --git a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/ContextTest.java b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/ContextTest.java
index 19401144d7b..c88cafe1940 100644
--- a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/ContextTest.java
+++ b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/ContextTest.java
@@ -19,6 +19,7 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import jakarta.annotation.Resource;
import org.apache.hertzbeat.alert.AlerterProperties;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
@@ -43,8 +44,8 @@
import org.apache.hertzbeat.common.queue.impl.InMemoryCommonDataQueue;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
-import org.apache.hertzbeat.log.controller.OtlpSignalController;
-import org.apache.hertzbeat.log.controller.ThreeSignalQueryController;
+import org.apache.hertzbeat.observability.controller.OtlpSignalController;
+import org.apache.hertzbeat.observability.controller.ThreeSignalQueryController;
import org.apache.hertzbeat.warehouse.WarehouseWorkerPool;
import org.apache.hertzbeat.warehouse.controller.MetricsDataController;
import org.apache.hertzbeat.warehouse.store.history.tsdb.iotdb.IotDbDataStorage;
@@ -54,6 +55,8 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* Manager Test
@@ -113,4 +116,14 @@ void testAutoImport() {
assertThrows(NoSuchBeanDefinitionException.class, () -> ctx.getBean(ThreeSignalQueryController.class));
}
+ @Test
+ void canonicalOtlpLogRouteShouldBeAvailableWithoutGreptime() {
+ RequestMappingHandlerMapping handlerMapping = ctx.getBean(
+ "requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
+
+ assertTrue(handlerMapping.getHandlerMethods().keySet().stream()
+ .anyMatch(mapping -> mapping.getPatternValues().contains("/api/otlp/v1/logs")
+ && mapping.getMethodsCondition().getMethods().contains(RequestMethod.POST)));
+ }
+
}
diff --git a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/EntityFreeObservabilityTransitionContractTest.java b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/EntityFreeObservabilityTransitionContractTest.java
new file mode 100644
index 00000000000..9b564a4e2e6
--- /dev/null
+++ b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/EntityFreeObservabilityTransitionContractTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.startup;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+
+class EntityFreeObservabilityTransitionContractTest {
+
+ private static final Path REPOSITORY_ROOT = repositoryRoot();
+
+ @Test
+ void reactorAndRuntimeShouldOwnOneObservabilityModule() throws IOException {
+ String rootPom = Files.readString(REPOSITORY_ROOT.resolve("pom.xml"));
+ String managerPom = Files.readString(REPOSITORY_ROOT.resolve("hertzbeat-manager/pom.xml"));
+
+ assertTrue(rootPom.contains("hertzbeat-observability"));
+ assertFalse(rootPom.contains("hertzbeat-log"));
+ assertTrue(managerPom.contains("hertzbeat-observability"));
+ assertFalse(managerPom.contains("hertzbeat-log"));
+ assertFalse(Files.exists(REPOSITORY_ROOT.resolve("hertzbeat-log")));
+ }
+
+ @Test
+ void productionSourcesShouldExposeOnlyTheCanonicalEntityFreeContract() throws IOException {
+ Path sourceRoot = REPOSITORY_ROOT.resolve("hertzbeat-observability/src/main/java");
+ assertTrue(Files.isDirectory(sourceRoot));
+
+ // The deprecated 1.8.x OTLP log ingestion aliases live in exactly one class so the
+ // 2.0 removal is a single file delete plus the matching Sureness rules.
+ Path legacyAliasController = sourceRoot.resolve(
+ "org/apache/hertzbeat/observability/controller/LegacyOtlpLogRouteController.java");
+ assertTrue(Files.isRegularFile(legacyAliasController));
+ String legacyAliasSource = Files.readString(legacyAliasController);
+ assertTrue(legacyAliasSource.contains("@Deprecated(since = \"1.9.0\", forRemoval = true)"));
+ assertTrue(legacyAliasSource.contains("/api/logs/otlp/v1/logs"));
+ assertTrue(legacyAliasSource.contains("/api/logs/ingest/{protocol}"));
+ assertTrue(legacyAliasSource.contains("logIngestionService.ingestHttp(content, headers)"));
+
+ String productionSources;
+ try (Stream sources = Files.walk(sourceRoot)) {
+ productionSources = sources
+ .filter(path -> path.toString().endsWith(".java"))
+ .filter(path -> !path.equals(legacyAliasController))
+ .map(EntityFreeObservabilityTransitionContractTest::readSource)
+ .reduce("", (left, right) -> left + '\n' + right);
+ }
+
+ assertTrue(productionSources.contains("/api/otlp/v1"));
+ assertTrue(productionSources.contains("/api/observability"));
+ assertFalse(productionSources.contains("/api/logs/otlp"));
+ assertFalse(productionSources.contains("/api/logs/ingest"));
+ assertFalse(productionSources.contains("/api/logs"));
+ assertFalse(productionSources.contains("/api/ingestion/otlp"));
+ assertFalse(productionSources.contains("/api/traces"));
+ assertFalse(productionSources.contains("org.apache.hertzbeat.manager.service.entity"));
+ assertFalse(productionSources.contains("HERTZBEAT_ENTITY_ID"));
+ assertFalse(productionSources.contains("HERTZBEAT_ENTITY_TYPE"));
+ assertFalse(productionSources.contains("EntityObservability"));
+ assertFalse(productionSources.contains("EntityTrace"));
+ assertFalse(productionSources.contains("OtlpEntity"));
+ }
+
+ @Test
+ void warehouseShouldOwnGreptimeSignalStorageSchemaAndQueries() throws IOException {
+ Path warehouseRoot = REPOSITORY_ROOT.resolve("hertzbeat-warehouse/src/main");
+ Path observabilityRoot = REPOSITORY_ROOT.resolve("hertzbeat-observability/src/main");
+
+ assertTrue(Files.isRegularFile(warehouseRoot.resolve(
+ "java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeSignalInitializer.java")));
+ assertTrue(Files.isRegularFile(warehouseRoot.resolve(
+ "java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeOtlpSignalStorage.java")));
+ assertTrue(Files.isRegularFile(warehouseRoot.resolve(
+ "java/org/apache/hertzbeat/warehouse/service/impl/GreptimeThreeSignalQueryService.java")));
+ assertTrue(Files.isRegularFile(warehouseRoot.resolve("resources/greptime/tables/hertzbeat_traces.sql")));
+ assertTrue(Files.isRegularFile(warehouseRoot.resolve(
+ "resources/greptime/pipelines/hertzbeat_otlp_log_v1.yaml")));
+
+ assertFalse(Files.exists(observabilityRoot.resolve(
+ "java/org/apache/hertzbeat/observability/config/GreptimeSignalInitializer.java")));
+ assertFalse(Files.exists(observabilityRoot.resolve(
+ "java/org/apache/hertzbeat/observability/service/impl/GreptimeThreeSignalQueryService.java")));
+ assertFalse(Files.exists(observabilityRoot.resolve("resources/greptime")));
+
+ String warehousePom = Files.readString(REPOSITORY_ROOT.resolve("hertzbeat-warehouse/pom.xml"));
+ assertFalse(warehousePom.contains("hertzbeat-observability"));
+ String observabilityForwarder = Files.readString(observabilityRoot.resolve(
+ "java/org/apache/hertzbeat/observability/service/impl/GreptimeOtlpSignalForwarder.java"));
+ assertTrue(observabilityForwarder.contains("OtlpSignalStorage"));
+ assertFalse(observabilityForwarder.contains("RestTemplate"));
+ }
+
+ @Test
+ void currentSecurityConfigurationsShouldProtectOnlyCanonicalObservabilityRoutes() throws IOException {
+ for (String relativePath : new String[] {
+ "hertzbeat-startup/src/main/resources/sureness.yml",
+ "hertzbeat-manager/src/test/resources/sureness.yml",
+ "hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/sureness.yml",
+ "script/sureness.yml",
+ "script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml",
+ "script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml",
+ "script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml",
+ "script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml",
+ "script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml"
+ }) {
+ String securityConfig = Files.readString(REPOSITORY_ROOT.resolve(relativePath));
+ assertTrue(securityConfig.contains("/api/otlp/v1/**===post===[admin,user]"), relativePath);
+ assertTrue(securityConfig.contains("/api/observability/**===get===[admin,user,guest]"), relativePath);
+ assertTrue(securityConfig.contains("/api/observability/logs===delete===[admin]"), relativePath);
+ // deprecated 1.8.x ingestion aliases keep the canonical write roles; no other legacy log rule may exist
+ assertTrue(securityConfig.contains("/api/logs/otlp/**===post===[admin,user]"), relativePath);
+ assertTrue(securityConfig.contains("/api/logs/ingest/**===post===[admin,user]"), relativePath);
+ assertFalse(securityConfig.replace("/api/logs/otlp/**===post===[admin,user]", "")
+ .replace("/api/logs/ingest/**===post===[admin,user]", "")
+ .contains("/api/logs/"), relativePath);
+ assertFalse(securityConfig.contains("/api/ingestion/otlp"), relativePath);
+ assertFalse(securityConfig.contains("/api/traces/"), relativePath);
+ assertFalse(securityConfig.contains("/api/otlp/**"), relativePath);
+ assertFalse(securityConfig.contains("/api/observability/capability===get"), relativePath);
+ }
+ }
+
+ @Test
+ void productAndSelfTelemetryShouldUseSeparateSignalTables() throws IOException {
+ String traceSchema = Files.readString(REPOSITORY_ROOT.resolve(
+ "hertzbeat-warehouse/src/main/resources/greptime/tables/hertzbeat_traces.sql"));
+ assertTrue(traceSchema.contains("CREATE TABLE IF NOT EXISTS hertzbeat_traces"));
+ assertFalse(traceSchema.contains("CREATE TABLE IF NOT EXISTS hzb_traces"));
+
+ String selfTelemetry = Files.readString(REPOSITORY_ROOT.resolve(
+ "hertzbeat-otel/src/main/java/org/apache/hertzbeat/otel/config/OpenTelemetryConfig.java"));
+ assertTrue(selfTelemetry.contains("DEFAULT_LOGS_TABLE_NAME = \"hzb_internal_logs\""));
+ assertTrue(selfTelemetry.contains("DEFAULT_TRACES_TABLE_NAME = \"hzb_internal_traces\""));
+ assertFalse(selfTelemetry.contains("DEFAULT_LOGS_TABLE_NAME = \"hertzbeat_logs\""));
+ assertFalse(selfTelemetry.contains("DEFAULT_TRACES_TABLE_NAME = \"hertzbeat_traces\""));
+ }
+
+ @Test
+ void currentClientsDocsAndProbesShouldUseCanonicalObservabilityRoutes() throws IOException {
+ for (String relativePath : new String[] {
+ "web-app/src/app/service/log.service.ts",
+ "web-app/src/app/service/observability.service.ts",
+ "web-app/src/app/routes/log/log-stream/log-stream.component.ts",
+ "hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/vector.yml"
+ }) {
+ String content = Files.readString(REPOSITORY_ROOT.resolve(relativePath));
+ assertFalse(content.contains("/api/logs"), relativePath);
+ assertFalse(content.contains("/logs/list"), relativePath);
+ assertFalse(content.contains("/logs/stats"), relativePath);
+ assertFalse(content.contains("/ingestion/otlp/metrics"), relativePath);
+ assertFalse(content.contains("/traces/list"), relativePath);
+ assertFalse(content.contains("/traces/stats"), relativePath);
+ }
+
+ // Integration docs must lead with the canonical route; the 1.8.x ingestion paths may only
+ // appear inside the upgrade notice that marks them as deprecated aliases.
+ for (String relativePath : new String[] {
+ "web-app/src/assets/doc/log-integration/otlp.en-US.md",
+ "web-app/src/assets/doc/log-integration/otlp.zh-CN.md",
+ "home/docs/help/log_integration.md",
+ "home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/log_integration.md"
+ }) {
+ String content = Files.readString(REPOSITORY_ROOT.resolve(relativePath));
+ assertTrue(content.contains("POST /api/otlp/v1/logs"), relativePath);
+ assertTrue(content.contains("/api/otlp/v1/logs"), relativePath);
+ assertTrue(content.contains("Deprecation: true"), relativePath);
+ assertFalse(content.contains("logs_endpoint: http://{hertzbeat_host}:1157/api/logs/"), relativePath);
+ assertFalse(content.contains("/logs/list"), relativePath);
+ assertFalse(content.contains("/logs/stats"), relativePath);
+ assertFalse(content.contains("/ingestion/otlp/metrics"), relativePath);
+ assertFalse(content.contains("/traces/list"), relativePath);
+ assertFalse(content.contains("/traces/stats"), relativePath);
+ }
+
+ // The upgrade guide must carry the 1.8.x -> 1.9.0 route table so operators see the break.
+ for (String relativePath : new String[] {
+ "home/docs/start/upgrade.md",
+ "home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/upgrade.md"
+ }) {
+ String content = Files.readString(REPOSITORY_ROOT.resolve(relativePath));
+ assertTrue(content.contains("`POST /api/logs/otlp/v1/logs` | `POST /api/otlp/v1/logs`"), relativePath);
+ assertTrue(content.contains("`GET /api/logs/list` | `GET /api/observability/logs`"), relativePath);
+ assertTrue(content.contains("`GET /api/traces/**` | `GET /api/observability/traces/**`"), relativePath);
+ }
+
+ String logService = Files.readString(REPOSITORY_ROOT.resolve("web-app/src/app/service/log.service.ts"));
+ assertTrue(logService.contains("/observability/logs"));
+ String observabilityService = Files.readString(
+ REPOSITORY_ROOT.resolve("web-app/src/app/service/observability.service.ts"));
+ assertTrue(observabilityService.contains("/observability/metrics/query"));
+ assertTrue(observabilityService.contains("/observability/traces"));
+ String streamComponent = Files.readString(REPOSITORY_ROOT.resolve(
+ "web-app/src/app/routes/log/log-stream/log-stream.component.ts"));
+ assertTrue(streamComponent.contains("/api/observability/logs/stream"));
+ String vectorConfig = Files.readString(REPOSITORY_ROOT.resolve(
+ "hertzbeat-e2e/hertzbeat-observability-e2e/src/test/resources/vector.yml"));
+ assertTrue(vectorConfig.contains("/api/otlp/v1/logs"));
+ }
+
+ private static String readSource(Path path) {
+ try {
+ return Files.readString(path);
+ } catch (IOException exception) {
+ throw new IllegalStateException("Failed to read " + path, exception);
+ }
+ }
+
+ private static Path repositoryRoot() {
+ Path candidate = Path.of("").toAbsolutePath();
+ while (candidate != null && !Files.isRegularFile(candidate.resolve("mvnw"))) {
+ candidate = candidate.getParent();
+ }
+ if (candidate == null) {
+ throw new IllegalStateException("Unable to locate repository root");
+ }
+ return candidate;
+ }
+}
diff --git a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessUnruledEndpointTest.java b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessUnruledEndpointTest.java
index 49ef4a867ed..be12696063a 100644
--- a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessUnruledEndpointTest.java
+++ b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessUnruledEndpointTest.java
@@ -78,21 +78,21 @@ void probingStorageAvailabilityStaysOpenToEveryRole() {
@Test
void deletingLogsIsRestrictedToAdmin() {
- assertEquals("[admin]", rolesFor("/api/logs", "delete"));
+ assertEquals("[admin]", rolesFor("/api/observability/logs", "delete"));
}
@Test
void readingLogsStaysOpenToEveryRole() {
- assertEquals("[admin,user,guest]", rolesFor("/api/logs/list", "get"));
+ assertEquals("[admin,user,guest]", rolesFor("/api/observability/logs", "get"));
}
/**
- * Matches how the sibling ingestion routes `/api/otlp/**` and `/api/logs/ingest/**`
- * are already scoped, so a low privileged account can no longer forge log records.
+ * Authenticated operators may write telemetry to the canonical OTLP boundary;
+ * guests may not.
*/
@Test
void ingestingOtlpLogsRequiresAtLeastUser() {
- assertEquals("[admin,user]", rolesFor("/api/logs/otlp/v1/logs", "post"));
+ assertEquals("[admin,user]", rolesFor("/api/otlp/v1/logs", "post"));
}
/**
diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/config/WarehouseAutoConfiguration.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/config/WarehouseAutoConfiguration.java
index ff8ef5ca20d..3ec129a4e9f 100644
--- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/config/WarehouseAutoConfiguration.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/config/WarehouseAutoConfiguration.java
@@ -19,7 +19,9 @@
import org.apache.hertzbeat.common.constants.ConfigConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
+import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
/**
@@ -31,5 +33,6 @@
@ComponentScan(basePackages = ConfigConstants.PkgConstant.PKG
+ SignConstants.DOT
+ ConfigConstants.FunctionModuleConstants.WAREHOUSE)
+@EnableConfigurationProperties(GreptimeProperties.class)
public class WarehouseAutoConfiguration {
}
diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/constants/WarehouseConstants.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/constants/WarehouseConstants.java
index fc0c325e406..d186941601e 100644
--- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/constants/WarehouseConstants.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/constants/WarehouseConstants.java
@@ -72,6 +72,8 @@ interface RealTimeName {
String LOG_TABLE_NAME = "hertzbeat_logs";
+ String TRACE_TABLE_NAME = "hertzbeat_traces";
+
String GREPTIME_QUERY_REST_TEMPLATE = "greptimeQueryRestTemplate";
String GREPTIME_WRITE_REST_TEMPLATE = "greptimeWriteRestTemplate";
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/LogProtocolAdapter.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/OtlpSignalStorage.java
similarity index 56%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/LogProtocolAdapter.java
rename to hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/OtlpSignalStorage.java
index a33e861c001..a9d27b88616 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/LogProtocolAdapter.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/OtlpSignalStorage.java
@@ -15,27 +15,17 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.service;
+package org.apache.hertzbeat.warehouse.service;
-/**
- * Adapter interface for ingesting logs pushed via different protocols
- * (e.g. OTLP, Loki, Filebeat, Vector).
- * Implementations should:
- * 1. Parse raw HTTP payload of their protocol.
- * 2. Convert data to LogEntry.
- * 3. Forward / persist it to downstream pipeline.
- */
-public interface LogProtocolAdapter {
+/** Storage boundary for validated OTLP protobuf signals. */
+public interface OtlpSignalStorage {
/**
- * Ingest log payload pushed from external system.
+ * Persist one validated OTLP export request.
*
- * @param content raw request body string
- */
- void ingest(String content);
-
- /**
- * Identifier of the protocol this adapter supports ("otlp", "vector", etc.)
+ * @param signal metrics, logs, or traces
+ * @param content encoded OTLP protobuf request
+ * @return encoded OTLP protobuf response
*/
- String supportProtocol();
-}
\ No newline at end of file
+ byte[] writeProtobuf(String signal, byte[] content);
+}
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/ThreeSignalQueryService.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/ThreeSignalQueryService.java
similarity index 98%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/ThreeSignalQueryService.java
rename to hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/ThreeSignalQueryService.java
index 5b5409f252e..8c1f0fb4a01 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/ThreeSignalQueryService.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/ThreeSignalQueryService.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.service;
+package org.apache.hertzbeat.warehouse.service;
import org.apache.hertzbeat.common.entity.dto.observability.OtlpMetricsConsole;
import org.apache.hertzbeat.common.entity.dto.observability.OtlpMetricsInventory;
diff --git a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/GreptimeThreeSignalQueryService.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/impl/GreptimeThreeSignalQueryService.java
similarity index 99%
rename from hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/GreptimeThreeSignalQueryService.java
rename to hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/impl/GreptimeThreeSignalQueryService.java
index dd85d7d2c57..c2476910807 100644
--- a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/service/impl/GreptimeThreeSignalQueryService.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/service/impl/GreptimeThreeSignalQueryService.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package org.apache.hertzbeat.log.service.impl;
+package org.apache.hertzbeat.warehouse.service.impl;
import java.net.URI;
import java.time.Instant;
@@ -40,9 +40,9 @@
import org.apache.hertzbeat.common.entity.dto.observability.TraceSpanNode;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.common.support.exception.StorageUnavailableException;
-import org.apache.hertzbeat.log.service.ThreeSignalQueryService;
import org.apache.hertzbeat.warehouse.db.GreptimeSqlQueryExecutor;
import org.apache.hertzbeat.warehouse.constants.WarehouseConstants;
+import org.apache.hertzbeat.warehouse.service.ThreeSignalQueryService;
import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -63,7 +63,7 @@
@ConditionalOnProperty(prefix = "warehouse.store.greptime", name = "enabled", havingValue = "true")
public class GreptimeThreeSignalQueryService implements ThreeSignalQueryService {
- private static final String TRACE_TABLE = "hzb_traces";
+ private static final String TRACE_TABLE = WarehouseConstants.TRACE_TABLE_NAME;
private static final Pattern SAFE_IDENTIFIER = Pattern.compile("[A-Za-z_:][A-Za-z0-9_:.-]*");
private static final Pattern SAFE_LABEL = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
private static final Set AGGREGATIONS = Set.of("sum", "avg", "min", "max", "count");
diff --git a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
index d0132de5b14..5ccbe4b84a5 100644
--- a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
+++ b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/greptime/GreptimeDbDataStorage.java
@@ -101,6 +101,10 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
private static final String LABEL_KEY_END_TIME = "end";
private static final String LABEL_KEY_TS = "ts";
private static final int LOG_BATCH_SIZE = 500;
+ private static final Map OTLP_RESOURCE_KEY_ALIASES = Map.of(
+ "service_name", "service.name",
+ "service_namespace", "service.namespace",
+ "deployment_environment_name", "deployment.environment.name");
private GreptimeDB greptimeDb;
@@ -756,7 +760,7 @@ private void buildObservabilityWhereConditions(StringBuilder sql, LogQueryFilter
addJsonCondition(resourceConditions, "deployment.environment.name", filter.environment());
if (StringUtils.hasText(filter.resourceFilter())) {
for (ResourceFilterExpression.Clause clause : ResourceFilterExpression.parse(filter.resourceFilter())) {
- String attribute = "json_get_string(resource, '$[\"" + clause.key() + "\"]')";
+ String attribute = resourceAttributeExpression(clause.key());
switch (clause.operator()) {
case EQUALS -> resourceConditions.add(attribute + " = '" + safeString(clause.value()) + "'");
case NOT_EQUALS -> resourceConditions.add(attribute + " <> '" + safeString(clause.value()) + "'");
@@ -774,10 +778,20 @@ private void buildObservabilityWhereConditions(StringBuilder sql, LogQueryFilter
private void addJsonCondition(List conditions, String key, String value) {
if (StringUtils.hasText(value)) {
- conditions.add("json_get_string(resource, '$[\"" + key + "\"]') = '" + safeString(value) + "'");
+ conditions.add(resourceAttributeExpression(key) + " = '" + safeString(value) + "'");
}
}
+ private String resourceAttributeExpression(String key) {
+ String normalizedKey = key.replace('.', '_');
+ String canonical = "json_get_string(resource, '$[\"" + key + "\"]')";
+ if (normalizedKey.equals(key)) {
+ return canonical;
+ }
+ String normalized = "json_get_string(resource, '$[\"" + normalizedKey + "\"]')";
+ return "COALESCE(" + canonical + ", " + normalized + ")";
+ }
+
private static long msToNs(Long ms) {
return ms * 1_000_000L;
}
@@ -857,7 +871,8 @@ private List mapRowsToLogEntries(List