This is an automated email from the ASF dual-hosted git repository.
tomsun28 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new 3ab68b5814 [improve] degrade observability consoles gracefully when
storage lacks log/trace/metric support (#4233)
3ab68b5814 is described below
commit 3ab68b581448c1f05fc58e04b41941c830bdf9c9
Author: Duansg <[email protected]>
AuthorDate: Fri Jul 24 00:54:05 2026 +0800
[improve] degrade observability consoles gracefully when storage lacks
log/trace/metric support (#4233)
Co-authored-by: Tomsun28 <[email protected]>
---
.../dto/observability/ObservabilityCapability.java | 28 +++++++
.../src/test/resources/sureness.yml | 1 +
.../log/controller/LogQueryController.java | 21 +++++-
.../ObservabilityCapabilityController.java | 65 +++++++++++++++++
.../log/controller/LogQueryControllerTest.java | 15 ++++
.../ObservabilityCapabilityControllerTest.java | 85 ++++++++++++++++++++++
hertzbeat-manager/src/test/resources/sureness.yml | 1 +
hertzbeat-startup/src/main/resources/sureness.yml | 1 +
.../store/history/tsdb/HistoryDataReader.java | 7 ++
.../tsdb/greptime/GreptimeDbDataStorage.java | 5 ++
.../hertzbeat-mysql-iotdb/conf/sureness.yml | 1 +
.../hertzbeat-mysql-tdengine/conf/sureness.yml | 1 +
.../conf/sureness.yml | 1 +
.../conf/sureness.yml | 1 +
.../conf/sureness.yml | 1 +
script/sureness.yml | 1 +
web-app/src/app/core/startup/startup.service.ts | 8 +-
.../log/log-manage/log-manage.component.html | 3 +-
.../log/log-manage/log-manage.component.spec.ts | 3 +-
.../routes/log/log-manage/log-manage.component.ts | 23 +++++-
.../routes/metrics/metrics-manage.component.html | 3 +-
.../metrics/metrics-manage.component.spec.ts | 3 +-
.../app/routes/metrics/metrics-manage.component.ts | 23 +++++-
.../app/routes/observability-navigation.spec.ts | 5 +-
.../routes/observability/signal-query-context.ts | 10 +++
.../signal-storage-guide.component.ts | 47 ++++++++++++
.../app/routes/trace/trace-manage.component.html | 3 +-
.../routes/trace/trace-manage.component.spec.ts | 6 +-
.../src/app/routes/trace/trace-manage.component.ts | 23 +++++-
web-app/src/assets/i18n/en-US.json | 3 +
web-app/src/assets/i18n/ja-JP.json | 3 +
web-app/src/assets/i18n/ko-KR.json | 3 +
web-app/src/assets/i18n/pt-BR.json | 3 +
web-app/src/assets/i18n/zh-CN.json | 3 +
web-app/src/assets/i18n/zh-TW.json | 3 +
35 files changed, 391 insertions(+), 22 deletions(-)
diff --git
a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/observability/ObservabilityCapability.java
b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/observability/ObservabilityCapability.java
new file mode 100644
index 0000000000..75ed6847f2
--- /dev/null
+++
b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/dto/observability/ObservabilityCapability.java
@@ -0,0 +1,28 @@
+/*
+ * 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.common.entity.dto.observability;
+
+/**
+ * Which observability query consoles the current storage backend supports.
+ *
+ * @param log log query console supported
+ * @param trace trace query console supported
+ * @param metric OTLP metric query console supported
+ */
+public record ObservabilityCapability(boolean log, boolean trace, boolean
metric) {
+}
diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
index 64228f740d..cdda55e93a 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
+++ b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
@@ -85,6 +85,7 @@ 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/src/main/java/org/apache/hertzbeat/log/controller/LogQueryController.java
b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogQueryController.java
index 07fb225d10..7cde958b43 100644
---
a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogQueryController.java
+++
b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/LogQueryController.java
@@ -24,6 +24,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Supplier;
import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader;
import org.springframework.data.domain.Page;
@@ -32,6 +33,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.constants.CommonConstants;
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;
@@ -54,6 +56,9 @@ public class LogQueryController {
private static final int MAX_PAGE_SIZE = 200;
+ private static final String LOG_STORAGE_UNAVAILABLE_MSG =
+ "Log query is not supported by the current storage, please enable
a log capable storage such as GreptimeDB";
+
private final HistoryDataReader historyDataReader;
private final SignalWorkloadGuard workloadGuard;
@@ -88,7 +93,7 @@ public class LogQueryController {
@RequestParam(value = "pageIndex", required = false, defaultValue
= "0") Integer pageIndex,
@Parameter(description = "Number of items per page", example =
"20")
@RequestParam(value = "pageSize", required = false, defaultValue =
"20") Integer pageSize) {
- return workloadGuard.execute(Workload.LOG_LIST, () -> {
+ return executeIfLogSupported(Workload.LOG_LIST, () -> {
LogQueryFilter filter = filter(start, end, traceId, spanId,
severityNumber, severityText, search,
serviceName, serviceNamespace, environment, resource);
Page<LogEntry> result = getPagedLogs(filter, pageIndex, pageSize);
@@ -118,7 +123,7 @@ public class LogQueryController {
@RequestParam(value = "serviceNamespace", required = false) String
serviceNamespace,
@RequestParam(value = "environment", required = false) String
environment,
@RequestParam(value = "resource", required = false) String
resource) {
- return workloadGuard.execute(Workload.LOG_AGGREGATE,
+ return executeIfLogSupported(Workload.LOG_AGGREGATE,
() -> doOverviewStats(filter(start, end, traceId, spanId,
severityNumber, severityText, search,
serviceName, serviceNamespace, environment,
resource)));
}
@@ -149,7 +154,7 @@ public class LogQueryController {
@RequestParam(value = "serviceNamespace", required = false) String
serviceNamespace,
@RequestParam(value = "environment", required = false) String
environment,
@RequestParam(value = "resource", required = false) String
resource) {
- return workloadGuard.execute(Workload.LOG_AGGREGATE,
+ return executeIfLogSupported(Workload.LOG_AGGREGATE,
() -> doTraceCoverageStats(filter(start, end, traceId, spanId,
severityNumber, severityText, search,
serviceName, serviceNamespace, environment,
resource)));
}
@@ -182,7 +187,7 @@ public class LogQueryController {
@RequestParam(value = "serviceNamespace", required = false) String
serviceNamespace,
@RequestParam(value = "environment", required = false) String
environment,
@RequestParam(value = "resource", required = false) String
resource) {
- return workloadGuard.execute(Workload.LOG_AGGREGATE,
+ return executeIfLogSupported(Workload.LOG_AGGREGATE,
() -> doTrendStats(filter(start, end, traceId, spanId,
severityNumber, severityText, search,
serviceName, serviceNamespace, environment,
resource)));
}
@@ -193,6 +198,14 @@ public class LogQueryController {
return ResponseEntity.ok(Message.success(result));
}
+ private <T> ResponseEntity<Message<T>> executeIfLogSupported(Workload
workload,
+
Supplier<ResponseEntity<Message<T>>> operation) {
+ if (!historyDataReader.supportsLogQuery()) {
+ return ResponseEntity.ok(Message.fail(CommonConstants.FAIL_CODE,
LOG_STORAGE_UNAVAILABLE_MSG));
+ }
+ return workloadGuard.execute(workload, operation);
+ }
+
private LogQueryFilter filter(Long start, Long end, String traceId, String
spanId, Integer severityNumber,
String severityText, String search, String
serviceName, String serviceNamespace,
String environment, String resource) {
diff --git
a/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityController.java
b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityController.java
new file mode 100644
index 0000000000..055167f403
--- /dev/null
+++
b/hertzbeat-log/src/main/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityController.java
@@ -0,0 +1,65 @@
+/*
+ * 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 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.store.history.tsdb.HistoryDataReader;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Report which observability query features the current storage backend
supports,
+ * so the UI can render guidance instead of firing queries that cannot be
served.
+ *
+ * <p>The three capabilities are probed through two different mechanisms on
purpose: log query
+ * is dispatched per storage via {@link HistoryDataReader#supportsLogQuery()}
(multiple storages
+ * implement log read/write), while trace/metric consoles are served only by
the conditionally
+ * registered {@link ThreeSignalQueryService} (GreptimeDB only for now).
+ * TODO: once trace/metric query grows storage-dispatched implementations like
logs (or log query
+ * is absorbed into the three-signal contract), unify the probes on a single
abstraction.
+ */
+@RestController
+@RequestMapping(path = "/api/observability", produces = "application/json")
+@Tag(name = "Observability Capability Controller")
+public class ObservabilityCapabilityController {
+
+ private final HistoryDataReader historyDataReader;
+ private final ObjectProvider<ThreeSignalQueryService>
threeSignalQueryService;
+
+ public ObservabilityCapabilityController(HistoryDataReader
historyDataReader,
+
ObjectProvider<ThreeSignalQueryService> threeSignalQueryService) {
+ this.historyDataReader = historyDataReader;
+ this.threeSignalQueryService = threeSignalQueryService;
+ }
+
+ @GetMapping("/capability")
+ @Operation(summary = "Query observability capability",
+ description = "Return whether log, trace and OTLP metric query
consoles are supported by the current storage backend.")
+ public ResponseEntity<Message<ObservabilityCapability>> capability() {
+ boolean traceAndMetricSupported =
threeSignalQueryService.getIfAvailable() != null;
+ return ResponseEntity.ok(Message.success(new ObservabilityCapability(
+ historyDataReader.supportsLogQuery(), traceAndMetricSupported,
traceAndMetricSupported)));
+ }
+}
diff --git
a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogQueryControllerTest.java
b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogQueryControllerTest.java
index 6aba088ddf..8dd39de96d 100644
---
a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogQueryControllerTest.java
+++
b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/LogQueryControllerTest.java
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.log.controller;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -57,12 +58,26 @@ class LogQueryControllerTest {
@BeforeEach
void setUp() {
+ lenient().when(historyDataReader.supportsLogQuery()).thenReturn(true);
this.logQueryController = new LogQueryController(historyDataReader,
new SignalWorkloadGuard());
this.mockMvc = MockMvcBuilders.standaloneSetup(logQueryController)
.setControllerAdvice(new SignalWorkloadExceptionHandler())
.build();
}
+ @Test
+ 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")) {
+ mockMvc.perform(MockMvcRequestBuilders.get(path))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.FAIL_CODE))
+ .andExpect(jsonPath("$.msg").isNotEmpty());
+ }
+ }
+
@Test
void testListLogsWithAllFilters() throws Exception {
// Mock data
diff --git
a/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityControllerTest.java
b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityControllerTest.java
new file mode 100644
index 0000000000..e406213d11
--- /dev/null
+++
b/hertzbeat-log/src/test/java/org/apache/hertzbeat/log/controller/ObservabilityCapabilityControllerTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.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.store.history.tsdb.HistoryDataReader;
+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.beans.factory.ObjectProvider;
+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 ObservabilityCapabilityController}
+ */
+@ExtendWith(MockitoExtension.class)
+class ObservabilityCapabilityControllerTest {
+
+ private MockMvc mockMvc;
+
+ @Mock
+ private HistoryDataReader historyDataReader;
+
+ @Mock
+ private ObjectProvider<ThreeSignalQueryService> threeSignalQueryService;
+
+ @Mock
+ private ThreeSignalQueryService queryService;
+
+ @BeforeEach
+ void setUp() {
+ this.mockMvc = MockMvcBuilders.standaloneSetup(
+ new ObservabilityCapabilityController(historyDataReader,
threeSignalQueryService)).build();
+ }
+
+ @Test
+ void shouldReportAllUnsupportedWithoutCapableStorage() throws Exception {
+ when(historyDataReader.supportsLogQuery()).thenReturn(false);
+ when(threeSignalQueryService.getIfAvailable()).thenReturn(null);
+
+
mockMvc.perform(MockMvcRequestBuilders.get("/api/observability/capability"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.SUCCESS_CODE))
+ .andExpect(jsonPath("$.data.log").value(false))
+ .andExpect(jsonPath("$.data.trace").value(false))
+ .andExpect(jsonPath("$.data.metric").value(false));
+ }
+
+ @Test
+ void shouldReportAllSupportedWithCapableStorage() throws Exception {
+ when(historyDataReader.supportsLogQuery()).thenReturn(true);
+
when(threeSignalQueryService.getIfAvailable()).thenReturn(queryService);
+
+
mockMvc.perform(MockMvcRequestBuilders.get("/api/observability/capability"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.SUCCESS_CODE))
+ .andExpect(jsonPath("$.data.log").value(true))
+ .andExpect(jsonPath("$.data.trace").value(true))
+ .andExpect(jsonPath("$.data.metric").value(true));
+ }
+}
diff --git a/hertzbeat-manager/src/test/resources/sureness.yml
b/hertzbeat-manager/src/test/resources/sureness.yml
index 64228f740d..cdda55e93a 100644
--- a/hertzbeat-manager/src/test/resources/sureness.yml
+++ b/hertzbeat-manager/src/test/resources/sureness.yml
@@ -85,6 +85,7 @@ 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/main/resources/sureness.yml
b/hertzbeat-startup/src/main/resources/sureness.yml
index b4e8e33d86..15e2a2b102 100644
--- a/hertzbeat-startup/src/main/resources/sureness.yml
+++ b/hertzbeat-startup/src/main/resources/sureness.yml
@@ -88,6 +88,7 @@ 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-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
index 468e6e0557..08a42bb919 100644
---
a/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
+++
b/hertzbeat-warehouse/src/main/java/org/apache/hertzbeat/warehouse/store/history/tsdb/HistoryDataReader.java
@@ -33,6 +33,13 @@ public interface HistoryDataReader {
*/
boolean isServerAvailable();
+ /**
+ * @return whether this storage supports observability log query
+ */
+ default boolean supportsLogQuery() {
+ return false;
+ }
+
/**
* query history range metrics data from tsdb
*
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 ae953720de..d0132de5b1 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
@@ -662,6 +662,11 @@ public class GreptimeDbDataStorage extends
AbstractHistoryDataStorage {
}
}
+ @Override
+ public boolean supportsLogQuery() {
+ return true;
+ }
+
@Override
public Map<String, Object> queryLogOverviewAggregate(LogQueryFilter
filter) {
try {
diff --git a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
index 282e93928a..70609bf26e 100644
--- a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
@@ -81,6 +81,7 @@ 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/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
b/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
index 282e93928a..70609bf26e 100644
--- a/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
@@ -81,6 +81,7 @@ 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/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
index 282e93928a..70609bf26e 100644
--- a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
@@ -81,6 +81,7 @@ 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/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
index 93d7529310..edc94a4ef4 100644
--- a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
@@ -85,6 +85,7 @@ 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/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
index 282e93928a..70609bf26e 100644
---
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
+++
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
@@ -81,6 +81,7 @@ 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/script/sureness.yml b/script/sureness.yml
index 23d0f4d143..47b0270e4c 100644
--- a/script/sureness.yml
+++ b/script/sureness.yml
@@ -85,6 +85,7 @@ 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/web-app/src/app/core/startup/startup.service.ts
b/web-app/src/app/core/startup/startup.service.ts
index 8ababf88b6..dcddafd5de 100644
--- a/web-app/src/app/core/startup/startup.service.ts
+++ b/web-app/src/app/core/startup/startup.service.ts
@@ -6,7 +6,7 @@ import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { ALAIN_I18N_TOKEN, Menu, MenuService, SettingsService, TitleService }
from '@delon/theme';
import type { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NzIconService } from 'ng-zorro-antd/icon';
-import { Observable, zip } from 'rxjs';
+import { Observable, of, zip } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { ICONS } from '../../../style-icons';
@@ -41,14 +41,15 @@ export class StartupService {
return zip(
this.i18n.loadLangData(defaultLang),
this.httpClient.get('./assets/app-data.json', { headers: headers }),
- this.httpClient.get(`/apps/hierarchy?lang=${defaultLang}`)
+ this.httpClient.get(`/apps/hierarchy?lang=${defaultLang}`),
+ this.httpClient.get('/observability/capability').pipe(catchError(() =>
of(null)))
).pipe(
catchError((res: NzSafeAny) => {
console.warn(`StartupService.load: Network request failed`, res);
setTimeout(() => this.router.navigateByUrl(`/exception/500`));
return [];
}),
- map(([langData, appData, menuData]: [Record<string, string>, NzSafeAny,
NzSafeAny]) => {
+ map(([langData, appData, menuData, capabilityData]: [Record<string,
string>, NzSafeAny, NzSafeAny, NzSafeAny]) => {
// setting language data
this.i18n.use(defaultLang, langData);
// Application information: including site name, description, year
@@ -85,6 +86,7 @@ export class StartupService {
item.hide = true;
}
});
+ this.storageService.putData('observabilityCapability',
capabilityData?.data ?? null);
this.storageService.putData('hierarchy', menuData.data);
this.menuService.resume();
this.titleService.suffix = appData.app.name;
diff --git a/web-app/src/app/routes/log/log-manage/log-manage.component.html
b/web-app/src/app/routes/log/log-manage/log-manage.component.html
index c2a4439eee..d70c4a7fd0 100644
--- a/web-app/src/app/routes/log/log-manage/log-manage.component.html
+++ b/web-app/src/app/routes/log/log-manage/log-manage.component.html
@@ -14,7 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<section class="signal-page">
+<app-signal-storage-guide *ngIf="!storageSupported"></app-signal-storage-guide>
+<section class="signal-page" *ngIf="storageSupported">
<header class="page-heading"
><div
><h1>{{ 'observability.logs.title' | i18n }}</h1
diff --git a/web-app/src/app/routes/log/log-manage/log-manage.component.spec.ts
b/web-app/src/app/routes/log/log-manage/log-manage.component.spec.ts
index 8a262b27df..b73477ac82 100644
--- a/web-app/src/app/routes/log/log-manage/log-manage.component.spec.ts
+++ b/web-app/src/app/routes/log/log-manage/log-manage.component.spec.ts
@@ -20,6 +20,7 @@
import { ActivatedRoute, Router } from '@angular/router';
import { LogService } from '../../../service/log.service';
+import { MemoryStorageService } from '../../../service/memory-storage.service';
import { LogManageComponent } from './log-manage.component';
describe('LogManageComponent', () => {
@@ -27,7 +28,7 @@ describe('LogManageComponent', () => {
const logs = jasmine.createSpyObj<LogService>('LogService', ['list',
'overviewStats', 'trendStats']);
const route = { snapshot: { queryParamMap: { get: () => null }, data: {} }
} as unknown as ActivatedRoute;
const router = jasmine.createSpyObj<Router>('Router', ['navigate']);
- const component = new LogManageComponent(logs, route, router);
+ const component = new LogManageComponent(logs, route, router, new
MemoryStorageService());
component.timeRange = [new Date(1_000), new Date(2_000)];
component.setMode('stream');
diff --git a/web-app/src/app/routes/log/log-manage/log-manage.component.ts
b/web-app/src/app/routes/log/log-manage/log-manage.component.ts
index 0c5f5281a4..97cda52845 100644
--- a/web-app/src/app/routes/log/log-manage/log-manage.component.ts
+++ b/web-app/src/app/routes/log/log-manage/log-manage.component.ts
@@ -33,9 +33,16 @@ import { Subject, debounceTime, finalize, forkJoin,
switchMap, takeUntil } from
import { LogEntry } from '../../../pojo/LogEntry';
import { LogOverviewStats, LogQueryOptions, LogService } from
'../../../service/log.service';
+import { MemoryStorageService } from '../../../service/memory-storage.service';
import { SignalContext } from '../../../service/observability.service';
import { SignalNavigationComponent } from
'../../observability/signal-navigation.component';
-import { moveSignalTimeRangeToNow, readSignalTimeRange, toSignalTimeContext }
from '../../observability/signal-query-context';
+import {
+ moveSignalTimeRangeToNow,
+ readSignalCapability,
+ readSignalTimeRange,
+ toSignalTimeContext
+} from '../../observability/signal-query-context';
+import { SignalStorageGuideComponent } from
'../../observability/signal-storage-guide.component';
import { SignalTimeRangeComponent } from
'../../observability/signal-time-range.component';
import { LogStreamComponent } from '../log-stream/log-stream.component';
@@ -63,6 +70,7 @@ export function trendBucketMillis(value: string): number {
NzTableModule,
NzTagModule,
SignalNavigationComponent,
+ SignalStorageGuideComponent,
SignalTimeRangeComponent,
LogStreamComponent
],
@@ -95,9 +103,20 @@ export class LogManageComponent implements OnInit,
OnDestroy {
private readonly queryChanges = new Subject<void>();
private readonly destroyed = new Subject<void>();
- constructor(private logsService: LogService, private route: ActivatedRoute,
private router: Router) {}
+ storageSupported = true;
+
+ constructor(
+ private logsService: LogService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private storage: MemoryStorageService
+ ) {}
ngOnInit(): void {
+ this.storageSupported = readSignalCapability(this.storage, 'log');
+ if (!this.storageSupported) {
+ return;
+ }
const params = this.route.snapshot.queryParamMap;
this.timeRange = readSignalTimeRange(params);
this.mode = params.get('view') === 'stream' ||
this.route.snapshot.data['logMode'] === 'stream' ? 'stream' : 'query';
diff --git a/web-app/src/app/routes/metrics/metrics-manage.component.html
b/web-app/src/app/routes/metrics/metrics-manage.component.html
index 55af41e68b..03dc7be18f 100644
--- a/web-app/src/app/routes/metrics/metrics-manage.component.html
+++ b/web-app/src/app/routes/metrics/metrics-manage.component.html
@@ -14,7 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<section class="signal-page">
+<app-signal-storage-guide *ngIf="!storageSupported"></app-signal-storage-guide>
+<section class="signal-page" *ngIf="storageSupported">
<header class="page-heading">
<div>
<h1>{{ 'observability.metrics.title' | i18n }}</h1>
diff --git a/web-app/src/app/routes/metrics/metrics-manage.component.spec.ts
b/web-app/src/app/routes/metrics/metrics-manage.component.spec.ts
index 304d49fd13..5a3eaf9768 100644
--- a/web-app/src/app/routes/metrics/metrics-manage.component.spec.ts
+++ b/web-app/src/app/routes/metrics/metrics-manage.component.spec.ts
@@ -21,6 +21,7 @@ import { fakeAsync, tick } from '@angular/core/testing';
import { ActivatedRoute, Router, convertToParamMap } from '@angular/router';
import { of } from 'rxjs';
+import { MemoryStorageService } from '../../service/memory-storage.service';
import { ObservabilityService } from '../../service/observability.service';
import { MetricsManageComponent } from './metrics-manage.component';
@@ -53,7 +54,7 @@ describe('MetricsManageComponent', () => {
snapshot: { queryParamMap: convertToParamMap({ start: 1_000, end: 2_000,
step: 15 }) }
} as unknown as ActivatedRoute;
const router = jasmine.createSpyObj<Router>('Router', ['navigate']);
- const component = new MetricsManageComponent(observability, route, router);
+ const component = new MetricsManageComponent(observability, route, router,
new MemoryStorageService());
component.ngOnInit();
tick(250);
diff --git a/web-app/src/app/routes/metrics/metrics-manage.component.ts
b/web-app/src/app/routes/metrics/metrics-manage.component.ts
index e68ce0b97f..a6d8dfe069 100644
--- a/web-app/src/app/routes/metrics/metrics-manage.component.ts
+++ b/web-app/src/app/routes/metrics/metrics-manage.component.ts
@@ -31,9 +31,16 @@ import { NzTableModule } from 'ng-zorro-antd/table';
import { NgxEchartsModule } from 'ngx-echarts';
import { Subject, debounceTime, finalize, switchMap, takeUntil } from 'rxjs';
+import { MemoryStorageService } from '../../service/memory-storage.service';
import { MetricSeries, ObservabilityService, SignalContext } from
'../../service/observability.service';
import { SignalNavigationComponent } from
'../observability/signal-navigation.component';
-import { moveSignalTimeRangeToNow, readSignalTimeRange, toSignalTimeContext }
from '../observability/signal-query-context';
+import {
+ moveSignalTimeRangeToNow,
+ readSignalCapability,
+ readSignalTimeRange,
+ toSignalTimeContext
+} from '../observability/signal-query-context';
+import { SignalStorageGuideComponent } from
'../observability/signal-storage-guide.component';
import { SignalTimeRangeComponent } from
'../observability/signal-time-range.component';
interface MetricTableRow {
@@ -56,6 +63,7 @@ interface MetricTableRow {
NzDrawerModule,
NzAutocompleteModule,
SignalNavigationComponent,
+ SignalStorageGuideComponent,
SignalTimeRangeComponent
],
templateUrl: './metrics-manage.component.html',
@@ -82,9 +90,20 @@ export class MetricsManageComponent implements OnInit,
OnDestroy {
private readonly queryChanges = new Subject<void>();
private readonly destroyed = new Subject<void>();
- constructor(private observability: ObservabilityService, private route:
ActivatedRoute, private router: Router) {}
+ storageSupported = true;
+
+ constructor(
+ private observability: ObservabilityService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private storage: MemoryStorageService
+ ) {}
ngOnInit(): void {
+ this.storageSupported = readSignalCapability(this.storage, 'metric');
+ if (!this.storageSupported) {
+ return;
+ }
const params = this.route.snapshot.queryParamMap;
this.timeRange = readSignalTimeRange(params);
this.serviceName = params.get('serviceName') || '';
diff --git a/web-app/src/app/routes/observability-navigation.spec.ts
b/web-app/src/app/routes/observability-navigation.spec.ts
index 47bbd76503..6ce9160e2d 100644
--- a/web-app/src/app/routes/observability-navigation.spec.ts
+++ b/web-app/src/app/routes/observability-navigation.spec.ts
@@ -20,6 +20,7 @@
import { ActivatedRoute, Router, convertToParamMap } from '@angular/router';
import { LogService } from '../service/log.service';
+import { MemoryStorageService } from '../service/memory-storage.service';
import { ObservabilityService } from '../service/observability.service';
import { LogManageComponent } from './log/log-manage/log-manage.component';
import { routes } from './routes-routing.module';
@@ -42,7 +43,7 @@ describe('Observability transition navigation', () => {
]);
const route = { snapshot: { queryParamMap: convertToParamMap({}) } } as
unknown as ActivatedRoute;
const router = jasmine.createSpyObj<Router>('Router', ['navigate']);
- const component = new TraceManageComponent(observability, route, router);
+ const component = new TraceManageComponent(observability, route, router,
new MemoryStorageService());
component.selected = {
summary: {
traceId: 'trace-1',
@@ -72,7 +73,7 @@ describe('Observability transition navigation', () => {
const logs = jasmine.createSpyObj<LogService>('LogService', ['list',
'overviewStats', 'trendStats']);
const route = { snapshot: { queryParamMap: convertToParamMap({}) } } as
unknown as ActivatedRoute;
const router = jasmine.createSpyObj<Router>('Router', ['navigate']);
- const component = new LogManageComponent(logs, route, router);
+ const component = new LogManageComponent(logs, route, router, new
MemoryStorageService());
component.timeRange = [new Date(1_000), new Date(2_000)];
component.openTrace({ traceId: 'trace-2', resource: { 'service.name':
'catalog' } });
diff --git a/web-app/src/app/routes/observability/signal-query-context.ts
b/web-app/src/app/routes/observability/signal-query-context.ts
index 7c757f72f2..92cdb0d6cf 100644
--- a/web-app/src/app/routes/observability/signal-query-context.ts
+++ b/web-app/src/app/routes/observability/signal-query-context.ts
@@ -19,11 +19,21 @@
import { ParamMap } from '@angular/router';
+import { MemoryStorageService } from '../../service/memory-storage.service';
+
export interface SignalTimeContext {
start?: number;
end?: number;
}
+export type SignalCapabilityKey = 'log' | 'trace' | 'metric';
+
+/** Reads the capability probe cached at startup; fail-open when the probe is
missing. */
+export function readSignalCapability(storage: MemoryStorageService, key:
SignalCapabilityKey): boolean {
+ const capability = storage.getData('observabilityCapability');
+ return capability == null || capability[key] !== false;
+}
+
export type SignalTimePreset = '15m' | '30m' | '1h' | '3h' | '6h' | '12h' |
'24h' | '7d';
export const SIGNAL_TIME_PRESETS: ReadonlyArray<{ value: SignalTimePreset;
durationMs: number }> = [
diff --git
a/web-app/src/app/routes/observability/signal-storage-guide.component.ts
b/web-app/src/app/routes/observability/signal-storage-guide.component.ts
new file mode 100644
index 0000000000..3d5720dd5b
--- /dev/null
+++ b/web-app/src/app/routes/observability/signal-storage-guide.component.ts
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+import { Component } from '@angular/core';
+import { I18nPipe } from '@delon/theme';
+import { NzButtonModule } from 'ng-zorro-antd/button';
+import { NzResultModule } from 'ng-zorro-antd/result';
+
+/**
+ * Guidance rendered by the observability consoles when the current
+ * storage backend cannot serve log/trace/metric queries.
+ */
+@Component({
+ selector: 'app-signal-storage-guide',
+ standalone: true,
+ imports: [I18nPipe, NzButtonModule, NzResultModule],
+ template: `
+ <nz-result
+ nzStatus="info"
+ [nzTitle]="'observability.storage.unsupported.title' | i18n"
+ [nzSubTitle]="'observability.storage.unsupported.description' | i18n"
+ >
+ <div nz-result-extra>
+ <a nz-button nzType="primary"
href="https://hertzbeat.apache.org/docs/start/greptime-init" target="_blank">
+ {{ 'observability.storage.unsupported.link' | i18n }}
+ </a>
+ </div>
+ </nz-result>
+ `
+})
+export class SignalStorageGuideComponent {}
diff --git a/web-app/src/app/routes/trace/trace-manage.component.html
b/web-app/src/app/routes/trace/trace-manage.component.html
index ab75188840..d0745946c7 100644
--- a/web-app/src/app/routes/trace/trace-manage.component.html
+++ b/web-app/src/app/routes/trace/trace-manage.component.html
@@ -14,7 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<section class="signal-page">
+<app-signal-storage-guide *ngIf="!storageSupported"></app-signal-storage-guide>
+<section class="signal-page" *ngIf="storageSupported">
<header class="page-heading">
<div
><h1>{{ 'observability.traces.title' | i18n }}</h1
diff --git a/web-app/src/app/routes/trace/trace-manage.component.spec.ts
b/web-app/src/app/routes/trace/trace-manage.component.spec.ts
index 044a995975..19cfd58f55 100644
--- a/web-app/src/app/routes/trace/trace-manage.component.spec.ts
+++ b/web-app/src/app/routes/trace/trace-manage.component.spec.ts
@@ -21,6 +21,7 @@ import { fakeAsync, tick } from '@angular/core/testing';
import { ActivatedRoute, Router, convertToParamMap } from '@angular/router';
import { of } from 'rxjs';
+import { MemoryStorageService } from '../../service/memory-storage.service';
import { ObservabilityService, TraceListItem, TraceSpanNode } from
'../../service/observability.service';
import { TraceManageComponent } from './trace-manage.component';
@@ -62,7 +63,8 @@ describe('TraceManageComponent', () => {
const component = new TraceManageComponent(
jasmine.createSpyObj<ObservabilityService>('ObservabilityService',
['queryTraces', 'traceOverview', 'traceDetail']),
{ snapshot: { queryParamMap: convertToParamMap({}) } } as unknown as
ActivatedRoute,
- jasmine.createSpyObj<Router>('Router', ['navigate'])
+ jasmine.createSpyObj<Router>('Router', ['navigate']),
+ new MemoryStorageService()
);
const root = span('root', '', 'STATUS_CODE_OK');
const child = span('child', 'root', 'STATUS_CODE_ERROR');
@@ -101,7 +103,7 @@ describe('TraceManageComponent', () => {
}
} as unknown as ActivatedRoute;
const router = jasmine.createSpyObj<Router>('Router', ['navigate']);
- const component = new TraceManageComponent(observability, route, router);
+ const component = new TraceManageComponent(observability, route, router,
new MemoryStorageService());
component.ngOnInit();
tick(250);
diff --git a/web-app/src/app/routes/trace/trace-manage.component.ts
b/web-app/src/app/routes/trace/trace-manage.component.ts
index 405d2f31d9..4476c0ab25 100644
--- a/web-app/src/app/routes/trace/trace-manage.component.ts
+++ b/web-app/src/app/routes/trace/trace-manage.component.ts
@@ -29,6 +29,7 @@ import { NzTableModule } from 'ng-zorro-antd/table';
import { NzTagModule } from 'ng-zorro-antd/tag';
import { Subject, debounceTime, finalize, forkJoin, switchMap, takeUntil }
from 'rxjs';
+import { MemoryStorageService } from '../../service/memory-storage.service';
import {
ObservabilityService,
SignalContext,
@@ -38,7 +39,13 @@ import {
TraceSpanNode
} from '../../service/observability.service';
import { SignalNavigationComponent } from
'../observability/signal-navigation.component';
-import { moveSignalTimeRangeToNow, readSignalTimeRange, toSignalTimeContext }
from '../observability/signal-query-context';
+import {
+ moveSignalTimeRangeToNow,
+ readSignalCapability,
+ readSignalTimeRange,
+ toSignalTimeContext
+} from '../observability/signal-query-context';
+import { SignalStorageGuideComponent } from
'../observability/signal-storage-guide.component';
import { SignalTimeRangeComponent } from
'../observability/signal-time-range.component';
@Component({
@@ -54,6 +61,7 @@ import { SignalTimeRangeComponent } from
'../observability/signal-time-range.com
NzTableModule,
NzTagModule,
SignalNavigationComponent,
+ SignalStorageGuideComponent,
SignalTimeRangeComponent
],
templateUrl: './trace-manage.component.html',
@@ -84,9 +92,20 @@ export class TraceManageComponent implements OnInit,
OnDestroy {
private readonly queryChanges = new Subject<void>();
private readonly destroyed = new Subject<void>();
- constructor(private observability: ObservabilityService, private route:
ActivatedRoute, private router: Router) {}
+ storageSupported = true;
+
+ constructor(
+ private observability: ObservabilityService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private storage: MemoryStorageService
+ ) {}
ngOnInit(): void {
+ this.storageSupported = readSignalCapability(this.storage, 'trace');
+ if (!this.storageSupported) {
+ return;
+ }
const params = this.route.snapshot.queryParamMap;
this.timeRange = readSignalTimeRange(params);
this.traceId = params.get('traceId') || '';
diff --git a/web-app/src/assets/i18n/en-US.json
b/web-app/src/assets/i18n/en-US.json
index c6b21da67f..1c4cc97836 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -739,6 +739,9 @@
"observability.traces.title": "Traces",
"observability.traces.total": "Total traces",
"observability.traces.results": "Trace results",
+ "observability.storage.unsupported.title": "Observability storage not
enabled",
+ "observability.storage.unsupported.description": "The current storage does
not support log, trace and metric queries. Please enable GreptimeDB
(warehouse.store.greptime) as the storage backend to use this feature.",
+ "observability.storage.unsupported.link": "How to enable GreptimeDB",
"common.button.query": "Query",
"log.stream.apply-filters": "Apply Filters",
"log.stream.apply-filters-tooltip": "Apply Filters",
diff --git a/web-app/src/assets/i18n/ja-JP.json
b/web-app/src/assets/i18n/ja-JP.json
index ab8970d8bb..e641b6252b 100644
--- a/web-app/src/assets/i18n/ja-JP.json
+++ b/web-app/src/assets/i18n/ja-JP.json
@@ -698,6 +698,9 @@
"observability.traces.title": "Traces",
"observability.traces.total": "Total traces",
"observability.traces.results": "Trace results",
+ "observability.storage.unsupported.title": "Observability storage not
enabled",
+ "observability.storage.unsupported.description": "The current storage does
not support log, trace and metric queries. Please enable GreptimeDB
(warehouse.store.greptime) as the storage backend to use this feature.",
+ "observability.storage.unsupported.link": "How to enable GreptimeDB",
"common.button.query": "Query",
"log.stream.apply-filters": "フィルターを適用",
"log.stream.apply-filters-tooltip": "フィルターを適用",
diff --git a/web-app/src/assets/i18n/ko-KR.json
b/web-app/src/assets/i18n/ko-KR.json
index f267512bdf..f9d570a991 100644
--- a/web-app/src/assets/i18n/ko-KR.json
+++ b/web-app/src/assets/i18n/ko-KR.json
@@ -915,6 +915,9 @@
"monitor.favorite.empty.history": "즐겨찾기한 히스토리 차트가 없습니다",
"monitor.favorite.add": "즐겨찾기 추가",
"monitor.favorite.remove": "즐겨찾기 제거",
+ "observability.storage.unsupported.title": "Observability storage not
enabled",
+ "observability.storage.unsupported.description": "The current storage does
not support log, trace and metric queries. Please enable GreptimeDB
(warehouse.store.greptime) as the storage backend to use this feature.",
+ "observability.storage.unsupported.link": "How to enable GreptimeDB",
"placeholder.key": "키",
"placeholder.value": "값",
"plugin.delete": "플러그인 삭제",
diff --git a/web-app/src/assets/i18n/pt-BR.json
b/web-app/src/assets/i18n/pt-BR.json
index 119e33ec80..d2e92d6230 100644
--- a/web-app/src/assets/i18n/pt-BR.json
+++ b/web-app/src/assets/i18n/pt-BR.json
@@ -467,6 +467,9 @@
"observability.traces.title": "Traces",
"observability.traces.total": "Total traces",
"observability.traces.results": "Trace results",
+ "observability.storage.unsupported.title": "Observability storage not
enabled",
+ "observability.storage.unsupported.description": "The current storage does
not support log, trace and metric queries. Please enable GreptimeDB
(warehouse.store.greptime) as the storage backend to use this feature.",
+ "observability.storage.unsupported.link": "How to enable GreptimeDB",
"common.button.query": "Query",
"log.stream.apply-filters": "Aplicar filtros",
"log.stream.apply-filters-tooltip": "Aplicar filtros",
diff --git a/web-app/src/assets/i18n/zh-CN.json
b/web-app/src/assets/i18n/zh-CN.json
index 222d5a91c6..d0b49fc070 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -742,6 +742,9 @@
"observability.traces.title": "链路",
"observability.traces.total": "链路总数",
"observability.traces.results": "链路结果",
+ "observability.storage.unsupported.title": "可观测性存储未启用",
+ "observability.storage.unsupported.description": "当前存储不支持日志、链路与指标查询,请启用
GreptimeDB(warehouse.store.greptime)作为存储后再使用该功能。",
+ "observability.storage.unsupported.link": "如何启用 GreptimeDB",
"common.button.query": "查询",
"log.stream.apply-filters": "应用过滤器",
"log.stream.apply-filters-tooltip": "应用过滤器",
diff --git a/web-app/src/assets/i18n/zh-TW.json
b/web-app/src/assets/i18n/zh-TW.json
index 6d55e53bc7..fc76f38dd2 100644
--- a/web-app/src/assets/i18n/zh-TW.json
+++ b/web-app/src/assets/i18n/zh-TW.json
@@ -702,6 +702,9 @@
"observability.traces.title": "鏈路",
"observability.traces.total": "鏈路總數",
"observability.traces.results": "鏈路結果",
+ "observability.storage.unsupported.title": "可觀測性儲存未啟用",
+ "observability.storage.unsupported.description": "目前儲存不支援日誌、鏈路與指標查詢,請啟用
GreptimeDB(warehouse.store.greptime)作為儲存後再使用該功能。",
+ "observability.storage.unsupported.link": "如何啟用 GreptimeDB",
"common.button.query": "查詢",
"log.stream.apply-filters": "應用過濾器",
"log.stream.apply-filters-tooltip": "應用過濾器",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]