[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:
Duansg
2026-07-24 00:54:05 +08:00
committed by GitHub
co-authored by Tomsun28
parent 1d9edb244c
commit 3ab68b5814
35 changed files with 391 additions and 22 deletions
@@ -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/**===*
@@ -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) {
@@ -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)));
}
}
@@ -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
@@ -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/**===*
@@ -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
*
@@ -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/**===*
+1
View File
@@ -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') || '';
+3
View File
@@ -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",
+3
View File
@@ -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": "フィルターを適用",
+3
View File
@@ -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": "플러그인 삭제",
+3
View File
@@ -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",
+3
View File
@@ -742,6 +742,9 @@
"observability.traces.title": "链路",
"observability.traces.total": "链路总数",
"observability.traces.results": "链路结果",
"observability.storage.unsupported.title": "可观测性存储未启用",
"observability.storage.unsupported.description": "当前存储不支持日志、链路与指标查询,请启用 GreptimeDBwarehouse.store.greptime)作为存储后再使用该功能。",
"observability.storage.unsupported.link": "如何启用 GreptimeDB",
"common.button.query": "查询",
"log.stream.apply-filters": "应用过滤器",
"log.stream.apply-filters-tooltip": "应用过滤器",
+3
View File
@@ -702,6 +702,9 @@
"observability.traces.title": "鏈路",
"observability.traces.total": "鏈路總數",
"observability.traces.results": "鏈路結果",
"observability.storage.unsupported.title": "可觀測性儲存未啟用",
"observability.storage.unsupported.description": "目前儲存不支援日誌、鏈路與指標查詢,請啟用 GreptimeDBwarehouse.store.greptime)作為儲存後再使用該功能。",
"observability.storage.unsupported.link": "如何啟用 GreptimeDB",
"common.button.query": "查詢",
"log.stream.apply-filters": "應用過濾器",
"log.stream.apply-filters-tooltip": "應用過濾器",