mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
[improve] degrade observability consoles gracefully when storage lacks log/trace/metric support (#4233)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
This commit is contained in:
+28
@@ -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) {
|
||||
}
|
||||
@@ -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/**===*
|
||||
|
||||
+17
-4
@@ -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) {
|
||||
|
||||
+65
@@ -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)));
|
||||
}
|
||||
}
|
||||
+15
@@ -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
|
||||
|
||||
+85
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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/**===*
|
||||
|
||||
@@ -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/**===*
|
||||
|
||||
+7
@@ -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
|
||||
*
|
||||
|
||||
+5
@@ -662,6 +662,11 @@ public class GreptimeDbDataStorage extends AbstractHistoryDataStorage {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsLogQuery() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> queryLogOverviewAggregate(LogQueryFilter filter) {
|
||||
try {
|
||||
|
||||
@@ -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/**===*
|
||||
|
||||
@@ -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/**===*
|
||||
|
||||
@@ -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/**===*
|
||||
|
||||
@@ -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/**===*
|
||||
|
||||
@@ -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/**===*
|
||||
|
||||
@@ -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/**===*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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') || '';
|
||||
|
||||
@@ -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' } });
|
||||
|
||||
@@ -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 }> = [
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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') || '';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "フィルターを適用",
|
||||
|
||||
@@ -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": "플러그인 삭제",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "应用过滤器",
|
||||
|
||||
@@ -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": "應用過濾器",
|
||||
|
||||
Reference in New Issue
Block a user