This is an automated email from the ASF dual-hosted git repository. zqr10159 pushed a commit to branch 2.0.0 in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
commit 3832485f0fe73bca9ebd2f9db86797e970fa3efa Author: Logic <[email protected]> AuthorDate: Wed Jun 3 11:40:48 2026 +0800 refactor: externalize backend localization messages --- .../impl/FeiShuAppAlertNotifyHandlerImpl.java | 14 +- .../notice/impl/FlyBookAlertNotifyHandlerImpl.java | 15 +- .../impl/FeiShuAppAlertNotifyHandlerImplTest.java | 17 ++- .../impl/FlyBookAlertNotifyHandlerImplTest.java | 16 ++- .../service/HuaweiCloudExternAlertServiceTest.java | 28 +++- .../hertzbeat/common/entity/job/Metrics.java | 3 +- .../entity/job/MetricsSourceLocalizationTest.java | 41 ++++++ .../dto/EvidenceDtoMigrationTest.java | 2 +- .../hertzbeat/common/util/CommonUtilTest.java | 4 +- .../common/entity/manager/ParamDefine.java | 2 +- .../manager/ParamDefineSourceLocalizationTest.java | 41 ++++++ .../EntityObservabilityDtoMigrationTest.java | 3 +- .../support/ResourceBundleUtf8ControlTest.java | 2 +- .../src/test/resources/msg.properties | 2 +- .../manager/controller/AppControllerTest.java | 4 +- .../manager/controller/MonitorControllerTest.java | 6 +- .../manager/service/ObserveEntityServiceTest.java | 7 +- ...ityDetailObservabilityReadModelServiceTest.java | 2 +- .../service/impl/OtlpGrpcIngestionServiceImpl.java | 26 ++-- .../service/impl/OtlpIngestionMessages.java | 45 ++++++ .../impl/OtlpIngestionWorkspaceServiceImpl.java | 156 +++++++++++++-------- .../impl/EntityObservabilityGatewayImpl.java | 59 ++++---- .../shared/service/impl/ObservabilityMessages.java | 45 ++++++ .../service/impl/TelemetryIntakeServiceImpl.java | 57 ++++---- .../main/resources/observability_en_US.properties | 120 ++++++++++++++++ .../main/resources/observability_zh_CN.properties | 120 ++++++++++++++++ .../controller/OtlpIngestionControllerTest.java | 5 +- ...OtlpIngestionServiceSourceLocalizationTest.java | 47 +++++++ .../OtlpIngestionWorkspaceServiceImplTest.java | 21 ++- .../impl/EntityObservabilityGatewayImplTest.java | 66 ++++++--- .../impl/TelemetryIntakeServiceImplTest.java | 40 +++++- 31 files changed, 832 insertions(+), 184 deletions(-) diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java index 51f961bff9..83dca2f0b4 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImpl.java @@ -293,9 +293,9 @@ public class FeiShuAppAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI "tag": "button", "text": { "tag": "plain_text", - "content": "登入控制台", + "content": "%s", "i18n_content": { - "en_us": "Login In" + "en_us": "%s" } }, "type": "default", @@ -326,9 +326,9 @@ public class FeiShuAppAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI "header": { "title": { "tag": "plain_text", - "content": "HertzBeat 告警", + "content": "%s", "i18n_content": { - "en_us": "HertzBeat Alarm" + "en_us": "%s" } }, "subtitle": { @@ -355,7 +355,11 @@ public class FeiShuAppAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI } String jsonStr = String.format(larkCardMessage, notificationContent.replace("\"", "\\\"") + atUserElement, - alerterProperties.getConsoleUrl()); + bundle.getString("alerter.notify.console"), + bundle.getString("alerter.notify.console"), + alerterProperties.getConsoleUrl(), + bundle.getString("alerter.notify.title"), + bundle.getString("alerter.notify.title")); return JsonUtil.fromJson(jsonStr); } diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImpl.java index ab61acb199..1aa7944a95 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImpl.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImpl.java @@ -137,9 +137,9 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl "tag": "button", "text": { "tag": "plain_text", - "content": "登入控制台", + "content": "%s", "i18n_content": { - "en_us": "Login In" + "en_us": "%s" } }, "type": "default", @@ -170,9 +170,9 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl "header": { "title": { "tag": "plain_text", - "content": "HertzBeat 告警", + "content": "%s", "i18n_content": { - "en_us": "HertzBeat Alarm" + "en_us": "%s" } }, "subtitle": { @@ -200,7 +200,12 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl return String.format(larkCardMessage, notificationContent.replace("\"", "\\\"") + atUserElement, - alerterProperties.getConsoleUrl(), TITLE_COLOR[priority]); + bundle.getString("alerter.notify.console"), + bundle.getString("alerter.notify.console"), + alerterProperties.getConsoleUrl(), + bundle.getString("alerter.notify.title"), + bundle.getString("alerter.notify.title"), + TITLE_COLOR[priority]); } @Override diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java index 8ce2607188..1a05827e97 100644 --- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FeiShuAppAlertNotifyHandlerImplTest.java @@ -26,9 +26,11 @@ import org.apache.hertzbeat.common.entity.alerter.SingleAlert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; @@ -37,8 +39,11 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.ResourceBundle; +import java.util.regex.Pattern; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -51,6 +56,8 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class FeiShuAppAlertNotifyHandlerImplTest { + private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{IsHan}"); + @Mock private RestTemplate restTemplate; @@ -92,7 +99,9 @@ class FeiShuAppAlertNotifyHandlerImplTest { template.setContent("test content"); lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + lenient().when(bundle.getString("alerter.notify.console")).thenReturn("Console Login"); lenient().when(alerterProperties.getConsoleUrl()).thenReturn("https://console.hertzbeat.com"); + feiShuAppAlertNotifyHandler.bundle = bundle; } /** @@ -116,6 +125,8 @@ class FeiShuAppAlertNotifyHandlerImplTest { new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse(); messageResp.setCode(0); messageResp.setMsg("success"); + ArgumentCaptor<HttpEntity<FeiShuAppAlertNotifyHandlerImpl.FeiShuAppMessageDto>> messageRequestCaptor = + ArgumentCaptor.forClass(HttpEntity.class); // Mock restTemplate calls when(restTemplate.exchange( @@ -128,10 +139,14 @@ class FeiShuAppAlertNotifyHandlerImplTest { when(restTemplate.exchange( anyString(), eq(org.springframework.http.HttpMethod.POST), - any(), + messageRequestCaptor.capture(), eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class))) .thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK)); feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert); + FeiShuAppAlertNotifyHandlerImpl.FeiShuAppMessageDto messageDto = messageRequestCaptor.getValue().getBody(); + assertTrue(messageDto.getContent().contains("Alert Notification"), messageDto.getContent()); + assertTrue(messageDto.getContent().contains("Console Login"), messageDto.getContent()); + assertFalse(HAN_SCRIPT.matcher(messageDto.getContent()).find(), messageDto.getContent()); } /** diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImplTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImplTest.java index 033de34613..61464c1e8e 100644 --- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImplTest.java +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/notice/impl/FlyBookAlertNotifyHandlerImplTest.java @@ -18,11 +18,14 @@ package org.apache.hertzbeat.alert.notice.impl; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import org.apache.hertzbeat.alert.AlerterProperties; +import org.springframework.http.HttpEntity; import org.apache.hertzbeat.common.entity.alerter.GroupAlert; import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver; import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; @@ -31,6 +34,7 @@ import org.apache.hertzbeat.alert.notice.AlertNoticeException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -42,6 +46,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.ResourceBundle; +import java.util.regex.Pattern; /** * Test case for FlyBook Alert Notify @@ -49,6 +54,8 @@ import java.util.ResourceBundle; @ExtendWith(MockitoExtension.class) class FlyBookAlertNotifyHandlerImplTest { + private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{IsHan}"); + @Mock private RestTemplate restTemplate; @@ -87,6 +94,8 @@ class FlyBookAlertNotifyHandlerImplTest { template.setContent("test content"); when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification"); + when(bundle.getString("alerter.notify.console")).thenReturn("Console Login"); + flyBookAlertNotifyHandler.bundle = bundle; } @Test @@ -101,14 +110,19 @@ class FlyBookAlertNotifyHandlerImplTest { successResp.setErrCode(0); ResponseEntity<CommonRobotNotifyResp> responseEntity = new ResponseEntity<>(successResp, HttpStatus.OK); + ArgumentCaptor<HttpEntity<String>> requestCaptor = ArgumentCaptor.forClass(HttpEntity.class); when(restTemplate.postForEntity( any(String.class), - any(), + requestCaptor.capture(), eq(CommonRobotNotifyResp.class) )).thenReturn(responseEntity); flyBookAlertNotifyHandler.send(receiver, template, groupAlert); + String body = requestCaptor.getValue().getBody(); + assertTrue(body.contains("\"content\": \"Alert Notification\"")); + assertTrue(body.contains("\"content\": \"Console Login\"")); + assertFalse(HAN_SCRIPT.matcher(body).find()); } @Test diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java index 3859303c7e..07ec65cadc 100644 --- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java @@ -77,7 +77,10 @@ public class HuaweiCloudExternAlertServiceTest { + "vaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwX" + "w1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9lYyeL7vVI0e4sfRbuQruz3E" + "0ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw=="); - externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已恢复正常。"); + externAlert.setSubject("[\u534e\u4e3a\u4e91][\u7d27\u6025\u544a\u8b66\u6062\u590d]" + + "\u4e91\u76d1\u63a7\u901a\u77e5:\u5206\u5e03\u5f0f\u7f13\u5b58\u670d\u52a1-DCS Redis" + + "\u5b9e\u4f8b “dcs-h4tv” \u7684\u6bcf\u79d2\u5e76\u53d1\u64cd\u4f5c\u6570" + + "\u5df2\u6062\u590d\u6b63\u5e38。"); externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX"); externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1"); externAlert.setType("Notification"); @@ -94,15 +97,23 @@ public class HuaweiCloudExternAlertServiceTest { externAlert.setSignature("Igs0bBhzw0JGmlgBH+9ejw2xWfPTXjAatAEsKDkkWcC5bZ/jveckdRZdgp/S0JER9eiJfMF427YDABufIN0sv/vBRXaRQKfRBLTJYbSTl+AQpEbIW5yUfJSRLEG3HNEhUDjASolbrW7zdPCoGkkqjifE23FCvw" + "+4tewMzqmHnfJHcFBq3W89CJzdPBjwO1UcY9C39moUZgqZk+qDVLpxb4bHSrEYAwPOSrOPR7TZpETJ30UOgFYajJydQk692edfs0NeVutHoQiOJ5/YC83ULHft0aXhichjtfZE4KF69nROAKez0ubk3l" + "Ey/mBIM9Ylbxn5b84OIrzzZQrIWe8Syw=="); - externAlert.setSubject("[华为云][紧急告警]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已触发告警。"); + externAlert.setSubject("[\u534e\u4e3a\u4e91][\u7d27\u6025\u544a\u8b66]" + + "\u4e91\u76d1\u63a7\u901a\u77e5:\u5206\u5e03\u5f0f\u7f13\u5b58\u670d\u52a1-DCS Redis" + + "\u5b9e\u4f8b “dcs-h4tv” \u7684\u6bcf\u79d2\u5e76\u53d1\u64cd\u4f5c\u6570" + + "\u5df2\u89e6\u53d1\u544a\u8b66。"); externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX"); externAlert.setMessageId("1565df032a19494590d61e05f7b0dc0e"); externAlert.setType("Notification"); - externAlert.setMessage("{\"version\":\"v1\",\"data\":{\"AccountName\":\"hid_hk6tij5o1v-95zn\",\"Namespace\":\"分布式缓存服务\",\"DimensionName\":\"DCS Redis实例\",\"ResourceName\"" - + ":\"dcs-h4tv\",\"MetricName\":\"每秒并发操作数\",\"IsAlarm\":true,\"AlarmLevel\":\"紧急\",\"Region\":\"华东-上海一\",\"RegionId\":\"cn-east-3\",\"ResourceId\":\"3dc7b9ea" + externAlert.setMessage("{\"version\":\"v1\",\"data\":{\"AccountName\":\"hid_hk6tij5o1v-95zn\"," + + "\"Namespace\":\"\u5206\u5e03\u5f0f\u7f13\u5b58\u670d\u52a1\"," + + "\"DimensionName\":\"DCS Redis\u5b9e\u4f8b\",\"ResourceName\"" + + ":\"dcs-h4tv\",\"MetricName\":\"\u6bcf\u79d2\u5e76\u53d1\u64cd\u4f5c\u6570\"," + + "\"IsAlarm\":true,\"AlarmLevel\":\"\u7d27\u6025\",\"Region\":\"\u534e\u4e1c-\u4e0a\u6d77\u4e00\"," + + "\"RegionId\":\"cn-east-3\",\"ResourceId\":\"3dc7b9ea" + "-70b4-4c38-942d-e2636e6d844c\",\"PrivateIp\":\"192.168.0.54\",\"CurrentData\":\"6.00 count\",\"AlarmTime\":\"2025/06/02 22:56:15 GMT+08:00\"," + "\"AlarmRecordID\":\"ah1748876175242njvndyzMZ\"," - + "\"AlarmRuleName\":\"alarm-c5jj\",\"IsOriginalValue\":true,\"Filter\":\"原始值\",\"ComparisonOperator\":\"\\u003e\",\"Value\":\"5 count\",\"Unit\":\"count\",\"Count\":2}}"); + + "\"AlarmRuleName\":\"alarm-c5jj\",\"IsOriginalValue\":true,\"Filter\":\"\u539f\u59cb\u503c\"," + + "\"ComparisonOperator\":\"\\u003e\",\"Value\":\"5 count\",\"Unit\":\"count\",\"Count\":2}}"); externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem"); externAlert.setTimestamp("2025-06-02T14:56:17Z"); externAlertService.addExternAlert(JsonUtil.toJson(externAlert)); @@ -135,7 +146,10 @@ public class HuaweiCloudExternAlertServiceTest { externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLbvaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np" + "1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwXw1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9l" + "YyeL7vVI0e4sfRbuQruz3E0+ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw=="); - externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已恢复正常。"); + externAlert.setSubject("[\u534e\u4e3a\u4e91][\u7d27\u6025\u544a\u8b66\u6062\u590d]" + + "\u4e91\u76d1\u63a7\u901a\u77e5:\u5206\u5e03\u5f0f\u7f13\u5b58\u670d\u52a1-DCS Redis" + + "\u5b9e\u4f8b “dcs-h4tv” \u7684\u6bcf\u79d2\u5e76\u53d1\u64cd\u4f5c\u6570" + + "\u5df2\u6062\u590d\u6b63\u5e38。"); externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX"); externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1"); externAlert.setType("UnsubscribeConfirmation"); @@ -184,4 +198,4 @@ public class HuaweiCloudExternAlertServiceTest { verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class)); } -} \ No newline at end of file +} diff --git a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java index 3b010bc399..ca10145bc9 100644 --- a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java +++ b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java @@ -87,8 +87,7 @@ public class Metrics { private String name; /** * metrics name's i18n value - * zh-CN: CPU信息 - * en-US: CPU Info + * Example: {"en-US": "CPU Info"} */ private Map<String, String> i18n; /** diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/MetricsSourceLocalizationTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/MetricsSourceLocalizationTest.java new file mode 100644 index 0000000000..7a633d8f1e --- /dev/null +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/entity/job/MetricsSourceLocalizationTest.java @@ -0,0 +1,41 @@ +/* + * 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.job; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + +class MetricsSourceLocalizationTest { + + private static final Path PRODUCTION_SOURCE = Path.of( + "src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java"); + private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{Script=Han}"); + + @Test + void productionSourceShouldNotContainHanScriptLiterals() throws IOException { + String source = Files.readString(PRODUCTION_SOURCE); + + assertFalse(HAN_SCRIPT.matcher(source).find(), + () -> "source file must not contain Han-script literals: " + PRODUCTION_SOURCE); + } +} diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/EvidenceDtoMigrationTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/EvidenceDtoMigrationTest.java index 8ebc8758da..9c8f57194c 100644 --- a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/EvidenceDtoMigrationTest.java +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/observability/dto/EvidenceDtoMigrationTest.java @@ -71,7 +71,7 @@ class EvidenceDtoMigrationTest { EntityResponseHandoffInfo traceHandoff = new EntityResponseHandoffInfo( "trace-1", "open", "critical", "checkout", "trace content", "trace-1", "span-1", "checkout", "commerce", "ERROR", "trace_id='trace-1'", "platform", "checkout-system", - "prod", 1000L, 2000L, "otlp", "trace", codeNavigationHint, "/entities/1", "返回实体" + "prod", 1000L, 2000L, "otlp", "trace", codeNavigationHint, "/entities/1", "Back to entity" ); EntityResponseHandoffsInfo handoffsInfo = new EntityResponseHandoffsInfo( null, null, null, traceHandoff, null, null diff --git a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/CommonUtilTest.java b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/CommonUtilTest.java index c99e77c977..be6f0eb5a0 100644 --- a/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/CommonUtilTest.java +++ b/hertzbeat-common-core/src/test/java/org/apache/hertzbeat/common/util/CommonUtilTest.java @@ -139,10 +139,10 @@ class CommonUtilTest { @Test void testGetLangMappingValueFromI18nMap() { Map<String, String> i18nMap = new HashMap<>(); - i18nMap.put("zh-CN", "中文"); + i18nMap.put("zh-CN", "Simplified Chinese"); i18nMap.put("ja", null); i18nMap.put("en-US", "English"); - assertEquals("中文", CommonUtil.getLangMappingValueFromI18nMap("zh-CN", i18nMap)); + assertEquals("Simplified Chinese", CommonUtil.getLangMappingValueFromI18nMap("zh-CN", i18nMap)); assertEquals("English", CommonUtil.getLangMappingValueFromI18nMap("en-US", i18nMap)); assertNull(CommonUtil.getLangMappingValueFromI18nMap("zh", new HashMap<>())); assertNotNull(CommonUtil.getLangMappingValueFromI18nMap("ja", i18nMap)); diff --git a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java index d4b54d2380..437bd43b01 100644 --- a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java +++ b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java @@ -73,7 +73,7 @@ public class ParamDefine { * Parameter field external display name * Port */ - @Schema(description = "The parameter field displays the internationalized name", example = "{zh-CN: '端口'}", + @Schema(description = "The parameter field displays the internationalized name", example = "{\"en-US\":\"Port\"}", accessMode = READ_WRITE) @Convert(converter = JsonMapAttributeConverter.class) @SuppressWarnings("JpaAttributeTypeInspection") diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/ParamDefineSourceLocalizationTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/ParamDefineSourceLocalizationTest.java new file mode 100644 index 0000000000..a5ca7b9801 --- /dev/null +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/entity/manager/ParamDefineSourceLocalizationTest.java @@ -0,0 +1,41 @@ +/* + * 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.manager; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + +class ParamDefineSourceLocalizationTest { + + private static final Path PRODUCTION_SOURCE = Path.of( + "src/main/java/org/apache/hertzbeat/common/entity/manager/ParamDefine.java"); + private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{Script=Han}"); + + @Test + void productionSourceShouldNotContainHanScriptLiterals() throws IOException { + String source = Files.readString(PRODUCTION_SOURCE); + + assertFalse(HAN_SCRIPT.matcher(source).find(), + () -> "source file must not contain Han-script literals: " + PRODUCTION_SOURCE); + } +} diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/observability/dto/entity/EntityObservabilityDtoMigrationTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/observability/dto/entity/EntityObservabilityDtoMigrationTest.java index 327114042d..d288774f89 100644 --- a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/observability/dto/entity/EntityObservabilityDtoMigrationTest.java +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/observability/dto/entity/EntityObservabilityDtoMigrationTest.java @@ -51,7 +51,8 @@ class EntityObservabilityDtoMigrationTest { EntityUnifiedEvidenceSummary unifiedSummary = new EntityUnifiedEvidenceSummary(3, true, true, false, 2L, 3, 0, 8L, List.of("metrics", "logs")); EntityTriageRecommendation recommendation = new EntityTriageRecommendation( - "rule", "metrics", "优先查看监控", "监控异常最多", "down monitors", "查看监控", 9L + "rule", "metrics", "Review monitors first", "Most monitors are abnormal", "down monitors", + "View monitors", 9L ); assertEquals("checkout-http", abnormalMonitor.getName()); diff --git a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8ControlTest.java b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8ControlTest.java index c30f5a29eb..6289642de2 100644 --- a/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8ControlTest.java +++ b/hertzbeat-common-spring/src/test/java/org/apache/hertzbeat/common/support/ResourceBundleUtf8ControlTest.java @@ -43,7 +43,7 @@ class ResourceBundleUtf8ControlTest { bundle = control.newBundle(baseName, Locale.ROOT, "java.properties", loader, false); assertNotNull(bundle); - assertEquals("你好", bundle.getString("hello")); + assertEquals("Ola, Mundo!", bundle.getString("hello")); } @Test diff --git a/hertzbeat-common-spring/src/test/resources/msg.properties b/hertzbeat-common-spring/src/test/resources/msg.properties index 6dc68e451d..8860d79098 100644 --- a/hertzbeat-common-spring/src/test/resources/msg.properties +++ b/hertzbeat-common-spring/src/test/resources/msg.properties @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -hello=你好 +hello=Ola, Mundo! diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AppControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AppControllerTest.java index 77c361687d..40b607ad27 100644 --- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AppControllerTest.java +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AppControllerTest.java @@ -71,7 +71,7 @@ class AppControllerTest { paramDefine.setField("port"); paramDefine.setType("number"); paramDefine.setDefaultValue("12"); - paramDefine.setPlaceholder("请输出密码"); + paramDefine.setPlaceholder("Enter password"); paramDefine.setCreator("tom"); paramDefine.setModifier("tom"); paramDefines.add(paramDefine); @@ -227,7 +227,7 @@ class AppControllerTest { void queryAppsHierarchy() throws Exception { // Data to make Hierarchy hierarchy = new Hierarchy(); - hierarchy.setLabel("Linux系统"); + hierarchy.setLabel("Linux system"); hierarchy.setValue("linux"); hierarchy.setCategory("os"); List<Hierarchy> list = new ArrayList<>(); diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java index 196afb121e..503323ab89 100644 --- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MonitorControllerTest.java @@ -62,7 +62,7 @@ class MonitorControllerTest { monitor.setName("TanCloud"); monitor.setInstance("192.167.25.11:8989"); monitor.setIntervals(600); - monitor.setDescription("对SAAS网站TanCloud的可用性监控"); + monitor.setDescription("Availability monitoring for the TanCloud SaaS site"); monitor.setCreator("tom"); monitor.setModifier("tom"); @@ -160,7 +160,7 @@ class MonitorControllerTest { monitor.setName("TanCloud"); monitor.setInstance("192.167.25.11:8989"); monitor.setIntervals(600); - monitor.setDescription("对SAAS网站TanCloud的可用性监控"); + monitor.setDescription("Availability monitoring for the TanCloud SaaS site"); monitor.setCreator("tom"); monitor.setModifier("tom"); @@ -203,7 +203,7 @@ class MonitorControllerTest { monitor.setName("TanCloud"); monitor.setInstance("192.167.25.11:8989"); monitor.setIntervals(600); - monitor.setDescription("对SAAS网站TanCloud的可用性监控"); + monitor.setDescription("Availability monitoring for the TanCloud SaaS site"); monitor.setCreator("tom"); monitor.setModifier("tom"); diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ObserveEntityServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ObserveEntityServiceTest.java index 0290444188..78fe8d4879 100644 --- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ObserveEntityServiceTest.java +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/ObserveEntityServiceTest.java @@ -37,6 +37,7 @@ import java.util.HashMap; import java.util.Collections; import java.util.List; import java.time.LocalDateTime; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -250,9 +251,12 @@ class ObserveEntityServiceTest { private EntityValidationService entityValidationService; private EntityWorkspaceAccessService entityWorkspaceAccessService; private EntityWorkspaceQueryService entityWorkspaceQueryService; + private Locale previousLocale; @BeforeEach void setUpNoiseControlDefaults() { + previousLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); telemetryIntakeService = org.mockito.Mockito.spy(new TelemetryIntakeServiceImpl(logQueryRepository)); entityObservabilityGateway = org.mockito.Mockito.spy( new EntityObservabilityGatewayImpl(telemetryIntakeService, entityTraceQueryService)); @@ -402,6 +406,7 @@ class ObserveEntityServiceTest { @AfterEach void tearDownRequestContext() { AuthTokenRequestContext.clear(); + Locale.setDefault(previousLocale); } @Test @@ -1330,7 +1335,7 @@ class ObserveEntityServiceTest { assertFalse(detail.getUnifiedEvidenceSummary().isTracesActive()); assertNotNull(detail.getTriageRecommendation()); assertEquals("metrics", detail.getTriageRecommendation().getRecommendedFocus()); - assertEquals("查看监控", detail.getTriageRecommendation().getActionLabel()); + assertEquals("View monitors", detail.getTriageRecommendation().getActionLabel()); assertTrue(detail.getOpsSummary().isOwnerReady()); assertTrue(detail.getOpsSummary().isRunbookReady()); assertTrue(detail.getOpsSummary().isRelationReady()); diff --git a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java index cb95e56700..ec4e2a36c0 100644 --- a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java +++ b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/entity/EntityDetailObservabilityReadModelServiceTest.java @@ -153,7 +153,7 @@ class EntityDetailObservabilityReadModelServiceTest { EntityUnifiedEvidenceSummary unifiedSummary = new EntityUnifiedEvidenceSummary( 2, true, true, false, 1, 1, 0, 987L, List.of("metrics", "logs")); EntityTriageRecommendation triage = new EntityTriageRecommendation( - "evidence", "metrics", "Metrics first", "Down monitor", "active alert", "查看监控", 987L); + "evidence", "metrics", "Metrics first", "Down monitor", "active alert", "View monitors", 987L); List<org.apache.hertzbeat.common.observability.dto.evidence.MetricEvidence> metricEvidence = Collections.emptyList(); List<org.apache.hertzbeat.common.observability.dto.evidence.LogEvidence> logEvidence = diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java index ae4b98867a..d5d3fbbe2a 100644 --- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java +++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpGrpcIngestionServiceImpl.java @@ -1633,7 +1633,7 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService { return observations; } return List.of(new MetricObservation("metric", null, null, baseMetricMetadata("unsupported", "unsupported", - "unsupported", "未知 OTLP metric 类型,当前未被 HertzBeat facade 识别。"))); + "unsupported", OtlpIngestionMessages.get("observability.otlp.metric.compatibility.unknown-type")))); } private MetricObservation fromNumberDataPoint(String metricType, @@ -1664,7 +1664,7 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService { "partial", "supported", "partial", - "Histogram 指标已写入 Greptime,但 HertzBeat 当前 facade 仅保留代表值与 bucket/bounds 元信息,未提供完整 histogram 查询语义。" + OtlpIngestionMessages.get("observability.otlp.metric.compatibility.histogram.reason") ); appendMetricTimeRange(metadata, point.getStartTimeUnixNano(), point.getTimeUnixNano()); metadata.put(OTLP_METRIC_DATA_POINT_COUNT, String.valueOf(dataPointCount)); @@ -1692,7 +1692,7 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService { "unsupported", "unsupported", "partial", - "Greptime 当前 OTLP metrics 数据模型不支持 ExponentialHistogram;HertzBeat 仅保留代表值与兼容性元信息。" + OtlpIngestionMessages.get("observability.otlp.metric.compatibility.exponential-histogram.reason") ); appendMetricTimeRange(metadata, point.getStartTimeUnixNano(), point.getTimeUnixNano()); metadata.put(OTLP_METRIC_DATA_POINT_COUNT, String.valueOf(dataPointCount)); @@ -1724,7 +1724,7 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService { "partial", "partial", "partial", - "Summary quantiles 语义受 Greptime 数据模型与当前 HertzBeat facade 限制,当前仅保留 summary/quantiles 元信息。" + OtlpIngestionMessages.get("observability.otlp.metric.compatibility.summary.reason") ); appendMetricTimeRange(metadata, point.getStartTimeUnixNano(), point.getTimeUnixNano()); metadata.put(OTLP_METRIC_DATA_POINT_COUNT, String.valueOf(dataPointCount)); @@ -1802,18 +1802,24 @@ public class OtlpGrpcIngestionServiceImpl implements OtlpGrpcIngestionService { metadata.put(OTLP_METRIC_COMPATIBILITY_REASON, overallReason); } if ("supported".equals(greptimeCompatibility)) { - metadata.put(OTLP_METRIC_GREPTIME_REASON, "Greptime 当前 OTLP metrics 数据模型支持该类型。"); + metadata.put(OTLP_METRIC_GREPTIME_REASON, + OtlpIngestionMessages.get("observability.otlp.metric.greptime.reason.supported")); } else if ("partial".equals(greptimeCompatibility)) { - metadata.put(OTLP_METRIC_GREPTIME_REASON, "Greptime 当前 OTLP metrics 数据模型仅部分保留该类型语义。"); + metadata.put(OTLP_METRIC_GREPTIME_REASON, + OtlpIngestionMessages.get("observability.otlp.metric.greptime.reason.partial")); } else { - metadata.put(OTLP_METRIC_GREPTIME_REASON, "Greptime 当前 OTLP metrics 数据模型不支持该类型。"); + metadata.put(OTLP_METRIC_GREPTIME_REASON, + OtlpIngestionMessages.get("observability.otlp.metric.greptime.reason.unsupported")); } if ("supported".equals(facadeCompatibility)) { - metadata.put(OTLP_METRIC_FACADE_REASON, "HertzBeat 当前 facade 可直接消费该类型。"); + metadata.put(OTLP_METRIC_FACADE_REASON, + OtlpIngestionMessages.get("observability.otlp.metric.facade.reason.supported")); } else if ("partial".equals(facadeCompatibility)) { - metadata.put(OTLP_METRIC_FACADE_REASON, "HertzBeat 当前 facade 仅保留代表值与兼容元信息。"); + metadata.put(OTLP_METRIC_FACADE_REASON, + OtlpIngestionMessages.get("observability.otlp.metric.facade.reason.partial")); } else { - metadata.put(OTLP_METRIC_FACADE_REASON, "HertzBeat 当前 facade 不支持该类型。"); + metadata.put(OTLP_METRIC_FACADE_REASON, + OtlpIngestionMessages.get("observability.otlp.metric.facade.reason.unsupported")); } return metadata; } diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionMessages.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionMessages.java new file mode 100644 index 0000000000..8b83bad27c --- /dev/null +++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionMessages.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.observability.ingestion.service.impl; + +import java.text.MessageFormat; +import java.util.MissingResourceException; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; + +/** + * Localized backend copy for OTLP ingestion DTOs. + */ +final class OtlpIngestionMessages { + + private static final String BUNDLE_NAME = "observability"; + + private OtlpIngestionMessages() { + } + + static String get(String key) { + try { + return ResourceBundleUtil.getBundle(BUNDLE_NAME).getString(key); + } catch (MissingResourceException exception) { + return key; + } + } + + static String format(String key, Object... args) { + return MessageFormat.format(get(key), args); + } +} diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java index 7efc672823..371509bcb2 100644 --- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java +++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java @@ -219,8 +219,10 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace || latestMetricSnapshot.getObservedAt() >= latestMonitorObservedAt)) { recentEvents.add(new OtlpIngestionOverviewDto.RecentSignalEvent( "metrics", - defaultText(latestMetricSnapshot.getServiceName(), "指标已接入"), - defaultText(latestMetricSnapshot.getServiceNamespace(), "最近已收到指标数据"), + defaultText(latestMetricSnapshot.getServiceName(), + message("observability.otlp.overview.event.metrics.title")), + defaultText(latestMetricSnapshot.getServiceNamespace(), + message("observability.otlp.overview.event.metrics.copy")), latestMetricSnapshot.getObservedAt() )); } else if (latestMonitor.isPresent()) { @@ -254,7 +256,9 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace monitorTotalCount + otlpMetricCount, metricsLatestObservedAt, metricsIntakeMode(monitorTotalCount, otlpMetricCount), - metricsActive ? "最近已收到可查看的指标数据" : "最近 24 小时还没有指标数据" + metricsActive + ? message("observability.otlp.overview.metrics.active") + : message("observability.otlp.overview.metrics.inactive") ), new OtlpIngestionOverviewDto.SignalOverview( "logs", @@ -262,7 +266,9 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace logTotalCount, logsLatestObservedAt, "OTLP", - logTotalCount > 0 ? "最近 24 小时已收到日志数据" : "最近 24 小时还没有日志数据" + logTotalCount > 0 + ? message("observability.otlp.overview.logs.active") + : message("observability.otlp.overview.logs.inactive") ), new OtlpIngestionOverviewDto.SignalOverview( "traces", @@ -270,7 +276,9 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace traceOverview.getTotalTraceCount(), traceOverview.getLatestObservedAt(), "OTLP", - traceOverview.getTotalTraceCount() > 0 ? "最近 24 小时已收到链路数据" : "最近 24 小时还没有链路数据" + traceOverview.getTotalTraceCount() > 0 + ? message("observability.otlp.overview.traces.active") + : message("observability.otlp.overview.traces.inactive") ), activeSignalCount, latestObservedAt, @@ -294,72 +302,90 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace long total = workspaceQueryGateway.countCollectors(); long online = workspaceQueryGateway.countCollectorsByStatus(CommonConstants.COLLECTOR_STATUS_ONLINE); if (total <= 0) { - return readinessCheck("collector", "Collector 集群", "warning", "未注册 Collector", - "先部署 Collector 或使用内置采集入口", checkedAt); + return readinessCheck("collector", message("observability.otlp.readiness.collector.title"), "warning", + message("observability.otlp.readiness.collector.unregistered"), + message("observability.otlp.readiness.collector.deploy"), checkedAt); } if (online >= total) { - return readinessCheck("collector", "Collector 集群", "success", online + " / " + total + " 在线", - "采集节点可接收任务", checkedAt); + return readinessCheck("collector", message("observability.otlp.readiness.collector.title"), "success", + message("observability.otlp.readiness.collector.online", online, total), + message("observability.otlp.readiness.collector.accepting"), checkedAt); } if (online > 0) { - return readinessCheck("collector", "Collector 集群", "warning", online + " / " + total + " 在线", - (total - online) + " 个采集节点离线", checkedAt); + return readinessCheck("collector", message("observability.otlp.readiness.collector.title"), "warning", + message("observability.otlp.readiness.collector.online", online, total), + message("observability.otlp.readiness.collector.offline", total - online), checkedAt); } - return readinessCheck("collector", "Collector 集群", "danger", "0 / " + total + " 在线", - "所有采集节点离线", checkedAt); + return readinessCheck("collector", message("observability.otlp.readiness.collector.title"), "danger", + message("observability.otlp.readiness.collector.online", 0, total), + message("observability.otlp.readiness.collector.all-offline"), checkedAt); } private OtlpIngestionOverviewDto.ReadinessCheck buildStorageReadiness(long checkedAt) { int total = historyDataReaders.size(); long available = countAvailableHistoryReaders(); if (total <= 0) { - return readinessCheck("storage", "历史存储", "warning", "未启用历史存储", - "检查历史存储配置", checkedAt); + return readinessCheck("storage", message("observability.otlp.readiness.storage.title"), "warning", + message("observability.otlp.readiness.storage.disabled"), + message("observability.otlp.readiness.storage.check-config"), checkedAt); } if (available >= total) { - return readinessCheck("storage", "历史存储", "success", available + " / " + total + " 可用", - "HistoryDataReader 可用", checkedAt); + return readinessCheck("storage", message("observability.otlp.readiness.storage.title"), "success", + message("observability.otlp.readiness.storage.available", available, total), + message("observability.otlp.readiness.storage.reader-available"), checkedAt); } if (available > 0) { - return readinessCheck("storage", "历史存储", "warning", available + " / " + total + " 可用", - "部分历史存储不可用", checkedAt); + return readinessCheck("storage", message("observability.otlp.readiness.storage.title"), "warning", + message("observability.otlp.readiness.storage.available", available, total), + message("observability.otlp.readiness.storage.partial"), checkedAt); } - return readinessCheck("storage", "历史存储", "danger", "0 / " + total + " 可用", - "检查历史存储配置", checkedAt); + return readinessCheck("storage", message("observability.otlp.readiness.storage.title"), "danger", + message("observability.otlp.readiness.storage.available", 0, total), + message("observability.otlp.readiness.storage.check-config"), checkedAt); } private OtlpIngestionOverviewDto.ReadinessCheck buildQueryReadiness(long checkedAt) { boolean promqlAvailable = hasPromqlExecutor(); boolean historyAvailable = countAvailableHistoryReaders() > 0; if (promqlAvailable && historyAvailable) { - return readinessCheck("query", "查询服务", "success", "指标、日志和链路查询可用", - "PromQL 与历史查询可用", checkedAt); + return readinessCheck("query", message("observability.otlp.readiness.query.title"), "success", + message("observability.otlp.readiness.query.available"), + message("observability.otlp.readiness.query.promql-history"), checkedAt); } if (promqlAvailable || historyAvailable) { - return readinessCheck("query", "查询服务", "warning", "部分查询能力可用", - promqlAvailable ? "PromQL 可用,历史查询待检查" : "历史查询可用,PromQL 待检查", checkedAt); + return readinessCheck("query", message("observability.otlp.readiness.query.title"), "warning", + message("observability.otlp.readiness.query.partial"), + promqlAvailable + ? message("observability.otlp.readiness.query.promql-only") + : message("observability.otlp.readiness.query.history-only"), checkedAt); } - return readinessCheck("query", "查询服务", "danger", "查询服务不可用", - "检查 PromQL 与历史查询配置", checkedAt); + return readinessCheck("query", message("observability.otlp.readiness.query.title"), "danger", + message("observability.otlp.readiness.query.unavailable"), + message("observability.otlp.readiness.query.check-config"), checkedAt); } private OtlpIngestionOverviewDto.ReadinessCheck buildGreptimeReadiness(long checkedAt) { boolean greptimeEnabled = greptimeProperties.stream().anyMatch(GreptimeProperties::enabled); if (!greptimeEnabled) { - return readinessCheck("greptime", "GreptimeDB", "neutral", "未启用 GreptimeDB", - "当前使用其他历史存储或尚未配置", checkedAt); + return readinessCheck("greptime", "GreptimeDB", "neutral", + message("observability.otlp.readiness.greptime.disabled"), + message("observability.otlp.readiness.greptime.other-storage"), checkedAt); } if (greptimeSqlQueryExecutors.isEmpty()) { - return readinessCheck("greptime", "GreptimeDB", "warning", "GreptimeDB 已启用,SQL 执行器未就绪", - "检查 GreptimeDB HTTP 配置", checkedAt); + return readinessCheck("greptime", "GreptimeDB", "warning", + message("observability.otlp.readiness.greptime.sql-not-ready"), + message("observability.otlp.readiness.greptime.check-http"), checkedAt); } try { greptimeSqlQueryExecutors.getFirst().execute("SELECT 1"); - return readinessCheck("greptime", "GreptimeDB", "success", "SQL 自检通过", - "SELECT 1 成功", checkedAt); + return readinessCheck("greptime", "GreptimeDB", "success", + message("observability.otlp.readiness.greptime.sql-ok"), + message("observability.otlp.readiness.greptime.select-ok"), checkedAt); } catch (RuntimeException exception) { - return readinessCheck("greptime", "GreptimeDB", "danger", "SQL 自检失败", - defaultText(exception.getMessage(), "检查 GreptimeDB 连接"), checkedAt); + return readinessCheck("greptime", "GreptimeDB", "danger", + message("observability.otlp.readiness.greptime.sql-failed"), + defaultText(exception.getMessage(), + message("observability.otlp.readiness.greptime.check-connection")), checkedAt); } } @@ -483,9 +509,9 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace "deployment.environment.name": "prod", }) - # 所有 signals 统一发送到 HertzBeat OTLP HTTP 入口: # %s - """.formatted(unifiedBaseEndpoint); + # %s + """.formatted(message("observability.otlp.guide.snippet.python.http.comment"), unifiedBaseEndpoint); String pythonGrpcSnippet = """ from opentelemetry.sdk.resources import Resource resource = Resource.create({ @@ -494,9 +520,9 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace "deployment.environment.name": "prod", }) - # 所有 signals 统一发送到 HertzBeat OTLP gRPC 入口: # %s - """.formatted(grpcEndpoint); + # %s + """.formatted(message("observability.otlp.guide.snippet.python.grpc.comment"), grpcEndpoint); return new OtlpIngestionGuideDto( "OTLP HTTP", @@ -510,57 +536,61 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace "http", "OTLP HTTP", metricsEndpoint, - "把指标数据发送到这个 OTLP HTTP 地址。收到数据后,可直接在监控和实体详情中继续查看。", - "如果已经配置了监控任务,数据会继续显示在对应的监控项中。" + message("observability.otlp.guide.metrics.http.description"), + message("observability.otlp.guide.metrics.http.note") ), new OtlpIngestionGuideDto.SignalGuide( "logs", "http", "OTLP HTTP", logsEndpoint, - "把日志数据发送到这个 OTLP HTTP 地址。收到数据后,可直接前往日志管理查看。", - "使用接入令牌即可完成认证。" + message("observability.otlp.guide.logs.http.description"), + message("observability.otlp.guide.logs.http.note") ), new OtlpIngestionGuideDto.SignalGuide( "traces", "http", "OTLP HTTP", traceEndpoint, - "把链路数据发送到这个 OTLP HTTP 地址。收到数据后,可直接前往链路管理查看。", - "系统会根据服务信息自动尝试关联到实体。" + message("observability.otlp.guide.traces.http.description"), + message("observability.otlp.guide.traces.http.note") ), new OtlpIngestionGuideDto.SignalGuide( "metrics", "grpc", "OTLP gRPC", grpcEndpoint, - "把指标数据发送到这个 OTLP gRPC 地址,适合已经使用 OpenTelemetry Collector 或语言 SDK 的服务。", - "收到数据后,可直接在监控和实体详情中继续查看。" + message("observability.otlp.guide.metrics.grpc.description"), + message("observability.otlp.guide.metrics.grpc.note") ), new OtlpIngestionGuideDto.SignalGuide( "logs", "grpc", "OTLP gRPC", grpcEndpoint, - "把日志数据发送到这个 OTLP gRPC 地址。收到数据后,可直接前往日志管理查看。", - "使用 Authorization Bearer 接入令牌即可完成认证。" + message("observability.otlp.guide.logs.grpc.description"), + message("observability.otlp.guide.logs.grpc.note") ), new OtlpIngestionGuideDto.SignalGuide( "traces", "grpc", "OTLP gRPC", grpcEndpoint, - "把链路数据发送到这个 OTLP gRPC 地址。收到数据后,可直接前往链路管理查看。", - "系统会根据服务信息自动尝试关联到实体。" + message("observability.otlp.guide.traces.grpc.description"), + message("observability.otlp.guide.traces.grpc.note") ) ), List.of( new OtlpIngestionGuideDto.Snippet("collector-http", "http", "OpenTelemetry Collector", "yaml", collectorSnippet), - new OtlpIngestionGuideDto.Snippet("java-http", "http", "Java 环境变量", "bash", javaSnippet), - new OtlpIngestionGuideDto.Snippet("python-http", "http", "Python 资源属性", "python", pythonSnippet), + new OtlpIngestionGuideDto.Snippet("java-http", "http", + message("observability.otlp.guide.snippet.java-env"), "bash", javaSnippet), + new OtlpIngestionGuideDto.Snippet("python-http", "http", + message("observability.otlp.guide.snippet.python-resource"), "python", pythonSnippet), new OtlpIngestionGuideDto.Snippet("collector-grpc", "grpc", "OpenTelemetry Collector", "yaml", collectorGrpcSnippet), - new OtlpIngestionGuideDto.Snippet("java-grpc", "grpc", "Java 环境变量", "bash", javaGrpcSnippet), - new OtlpIngestionGuideDto.Snippet("python-grpc", "grpc", "Python 资源属性", "python", pythonGrpcSnippet) + new OtlpIngestionGuideDto.Snippet("java-grpc", "grpc", + message("observability.otlp.guide.snippet.java-env"), "bash", javaGrpcSnippet), + new OtlpIngestionGuideDto.Snippet("python-grpc", "grpc", + message("observability.otlp.guide.snippet.python-resource"), "python", pythonGrpcSnippet) ) ); } @@ -666,7 +696,7 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace null, new OtlpMetricsConsoleDto.Stats(0, 0, null), "no_context", - "缺少可用于构建 OTLP 指标查询的服务上下文。" + message("observability.otlp.metrics-console.no-context") ); } if (!metricQueryRepository.hasPromqlExecutor()) { @@ -678,7 +708,7 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace null, new OtlpMetricsConsoleDto.Stats(0, 0, null), "load_failed", - "当前环境未配置可用的 PromQL 指标查询执行器。" + message("observability.otlp.metrics-console.promql-unavailable") ); } MetricsQueryExecution execution = executeMetricsConsoleQuery(resolvedQuery, resolvedStart, resolvedEnd); @@ -1166,12 +1196,20 @@ public class OtlpIngestionWorkspaceServiceImpl implements OtlpIngestionWorkspace private String metricsIntakeMode(long monitorTotalCount, long otlpMetricCount) { if (monitorTotalCount > 0 && otlpMetricCount > 0) { - return "OTLP + 实体监控"; + return message("observability.otlp.overview.metrics.mode.mixed"); } if (otlpMetricCount > 0) { return "OTLP"; } - return "实体监控"; + return message("observability.otlp.overview.metrics.mode.monitor"); + } + + private static String message(String key) { + return OtlpIngestionMessages.get(key); + } + + private static String message(String key, Object... args) { + return OtlpIngestionMessages.format(key, args); } private String defaultText(String primary, String fallback) { diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java index ac65e7546a..b21b6d109e 100644 --- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java +++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java @@ -265,80 +265,80 @@ public class EntityObservabilityGatewayImpl implements EntityObservabilityGatewa if (activeAlertCount > 0) { actions.add(new EntityNextActionInfo( "review_alerts", - "处理活跃告警", - activeAlertCount + " 条活跃告警已经关联到当前实体,先确认影响范围和当前状态。", - "查看活跃告警", + ObservabilityMessages.get("observability.entity.next.review-alerts.title"), + ObservabilityMessages.format("observability.entity.next.review-alerts.description", activeAlertCount), + ObservabilityMessages.get("observability.entity.next.review-alerts.action"), 100 )); } if (opsSummary != null && !opsSummary.isOwnerReady()) { actions.add(new EntityNextActionInfo( "complete_owner", - "补负责人", - "当前实体还没有明确负责人,排障和治理动作缺少责任归属。", - "补负责人", + ObservabilityMessages.get("observability.entity.next.complete-owner.title"), + ObservabilityMessages.get("observability.entity.next.complete-owner.description"), + ObservabilityMessages.get("observability.entity.next.complete-owner.action"), 90 )); } if (opsSummary != null && !opsSummary.isRunbookReady()) { actions.add(new EntityNextActionInfo( "complete_runbook", - "补处置手册", - "先补一条 runbook 或处置入口,告警后才能直接推动恢复。", - "补处置手册", + ObservabilityMessages.get("observability.entity.next.complete-runbook.title"), + ObservabilityMessages.get("observability.entity.next.complete-runbook.description"), + ObservabilityMessages.get("observability.entity.next.complete-runbook.action"), 80 )); } if (evidenceSummary != null && healthyMonitorCount + downMonitorCount == 0) { actions.add(new EntityNextActionInfo( "bind_monitor", - "补绑监控", - "实体已经存在,但还没有绑定任何监控任务,当前状态只能停留在目录元数据层。", - "关联监控", + ObservabilityMessages.get("observability.entity.next.bind-monitor.title"), + ObservabilityMessages.get("observability.entity.next.bind-monitor.description"), + ObservabilityMessages.get("observability.entity.next.bind-monitor.action"), 75 )); } else if (downMonitorCount > 0 && activeAlertCount == 0) { actions.add(new EntityNextActionInfo( "bind_monitor", - "查看异常监控", - downMonitorCount + " 个监控当前异常,建议先确认哪个证据最值得继续排查。", - "查看核心监控", + ObservabilityMessages.get("observability.entity.next.abnormal-monitors.title"), + ObservabilityMessages.format("observability.entity.next.abnormal-monitors.description", downMonitorCount), + ObservabilityMessages.get("observability.entity.next.abnormal-monitors.action"), 74 )); } if (opsSummary != null && !opsSummary.isTelemetryReady()) { actions.add(new EntityNextActionInfo( "open_discovery", - "归并证据到实体", - "当前还没有形成足够的监控、身份或日志线索,建议先回到发现工作台归并证据。", - "打开发现工作台", + ObservabilityMessages.get("observability.entity.next.open-discovery.title"), + ObservabilityMessages.get("observability.entity.next.open-discovery.description"), + ObservabilityMessages.get("observability.entity.next.open-discovery.action"), 70 )); } if (logSummary != null && logSummary.getHintCount() > 0 && activeAlertCount == 0) { actions.add(new EntityNextActionInfo( "inspect_logs", - "查看日志线索", - "日志入口已经具备,先确认 OTel resource 或 fallback 查询是否能快速定位异常。", - "查看日志线索", + ObservabilityMessages.get("observability.entity.next.inspect-logs.title"), + ObservabilityMessages.get("observability.entity.next.inspect-logs.description"), + ObservabilityMessages.get("observability.entity.next.inspect-logs.action"), 60 )); } if (opsSummary != null && !opsSummary.isRelationReady()) { actions.add(new EntityNextActionInfo( "review_relations", - "补关键关系", - "关键上下游关系还不完整,建议先补齐最关键的依赖边界。", - "查看关系", + ObservabilityMessages.get("observability.entity.next.review-relations.title"), + ObservabilityMessages.get("observability.entity.next.review-relations.description"), + ObservabilityMessages.get("observability.entity.next.review-relations.action"), 50 )); } if (actions.isEmpty()) { actions.add(new EntityNextActionInfo( "inspect_logs", - "继续排查当前实体", - "目录归属和证据都已经具备,可以直接从日志或监控继续深入分析。", - "查看日志线索", + ObservabilityMessages.get("observability.entity.next.fallback.title"), + ObservabilityMessages.get("observability.entity.next.fallback.description"), + ObservabilityMessages.get("observability.entity.next.fallback.action"), 10 )); } @@ -366,11 +366,12 @@ public class EntityObservabilityGatewayImpl implements EntityObservabilityGatewa @Override public String buildEntityReturnLabel(ObservedEntityContext entityContext) { if (entityContext == null || entityContext.getEntity() == null) { - return "实体详情"; + return ObservabilityMessages.get("observability.entity.return-label.fallback"); } return defaultText( trimToNull(entityContext.getEntity().getDisplayName()), - defaultText(trimToNull(entityContext.getEntity().getName()), "实体详情") + defaultText(trimToNull(entityContext.getEntity().getName()), + ObservabilityMessages.get("observability.entity.return-label.fallback")) ); } diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/ObservabilityMessages.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/ObservabilityMessages.java new file mode 100644 index 0000000000..b1d4c93b77 --- /dev/null +++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/ObservabilityMessages.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.observability.shared.service.impl; + +import java.text.MessageFormat; +import java.util.MissingResourceException; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; + +/** + * Localized backend copy for observability DTOs. + */ +final class ObservabilityMessages { + + private static final String BUNDLE_NAME = "observability"; + + private ObservabilityMessages() { + } + + static String get(String key) { + try { + return ResourceBundleUtil.getBundle(BUNDLE_NAME).getString(key); + } catch (MissingResourceException exception) { + return key; + } + } + + static String format(String key, Object... args) { + return MessageFormat.format(get(key), args); + } +} diff --git a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java index cb79db53b0..7b53fa7675 100644 --- a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java +++ b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java @@ -591,7 +591,7 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway { preferredMonitor == null ? Collections.emptyList() : List.of(defaultText(preferredMonitor.getName(), preferredMonitor.getInstance())), defaultText(preferredMonitor == null ? null : preferredMonitor.getName(), "monitor")), "entity.monitor.availability", - "实体监控状态", + ObservabilityMessages.get("observability.telemetry.metric.entity-monitor-status"), "summary", "count", (double) healthyCount, @@ -965,21 +965,28 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway { && !"normal".equalsIgnoreCase(item.getSeverityOrHealth())); if (metricsRequireAttention) { String summary = activeAlerts > 0 - ? "当前已有活跃告警,建议先确认监控面是否正在持续异常。" - : "当前监控面已经出现异常或波动,建议先从指标和监控状态入手。"; + ? ObservabilityMessages.get("observability.telemetry.triage.metrics.alert.summary") + : ObservabilityMessages.get("observability.telemetry.triage.metrics.monitor.summary"); String whyNow = activeAlerts > 0 - ? "活跃告警通常意味着影响已经明确,可先查看监控和告警上下文。" - : "异常监控最能快速反映健康度变化,适合作为第一观察面。"; - return new EntityTriageRecommendation("rule", FOCUS_METRICS, "优先查看监控", summary, whyNow, "查看监控", now); + ? ObservabilityMessages.get("observability.telemetry.triage.metrics.alert.reason") + : ObservabilityMessages.get("observability.telemetry.triage.metrics.monitor.reason"); + return new EntityTriageRecommendation( + "rule", + FOCUS_METRICS, + ObservabilityMessages.get("observability.telemetry.triage.metrics.title"), + summary, + whyNow, + ObservabilityMessages.get("observability.telemetry.triage.metrics.action"), + now); } if (traceSummary != null && traceSummary.getRecentErrorTraceCount() > 0 || !CollectionUtils.isEmpty(traceEvidence)) { return new EntityTriageRecommendation( "rule", FOCUS_TRACES, - "优先查看链路", - "当前链路侧已经出现错误或活跃调用,适合先查看调用路径和错误跨度。", - "链路最容易帮助定位请求经过了哪些组件,以及失败发生在什么位置。", - "查看链路", + ObservabilityMessages.get("observability.telemetry.triage.traces.title"), + ObservabilityMessages.get("observability.telemetry.triage.traces.summary"), + ObservabilityMessages.get("observability.telemetry.triage.traces.reason"), + ObservabilityMessages.get("observability.telemetry.triage.traces.action"), now ); } @@ -987,20 +994,20 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway { return new EntityTriageRecommendation( "rule", FOCUS_LOGS, - "优先查看日志", - "当前已有可直接使用的日志检索线索,适合先查看最近错误文本和上下文。", - "日志通常能最快补全异常细节,帮助确认具体报错和影响范围。", - "查看日志", + ObservabilityMessages.get("observability.telemetry.triage.logs.title"), + ObservabilityMessages.get("observability.telemetry.triage.logs.summary"), + ObservabilityMessages.get("observability.telemetry.triage.logs.reason"), + ObservabilityMessages.get("observability.telemetry.triage.logs.action"), now ); } return new EntityTriageRecommendation( "rule", FOCUS_EVIDENCE, - "继续补充证据", - "当前还没有足够强的三信号线索,建议先确认接入状态或补充更多运行数据。", - "先确认最近是否已经收到指标、日志或链路数据,再继续定位。", - "查看证据", + ObservabilityMessages.get("observability.telemetry.triage.evidence.title"), + ObservabilityMessages.get("observability.telemetry.triage.evidence.summary"), + ObservabilityMessages.get("observability.telemetry.triage.evidence.reason"), + ObservabilityMessages.get("observability.telemetry.triage.evidence.action"), now ); } @@ -1269,12 +1276,12 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway { List<String> segments = new ArrayList<>(); if (StringUtils.hasText(compatibility)) { - segments.add("OTLP metrics 兼容性:" + switch (compatibility) { - case "supported" -> "支持"; - case "partial" -> "部分支持"; - case "unsupported" -> "不支持"; + segments.add(ObservabilityMessages.format("observability.telemetry.metric.context.compatibility", switch (compatibility) { + case "supported" -> ObservabilityMessages.get("observability.telemetry.metric.context.supported"); + case "partial" -> ObservabilityMessages.get("observability.telemetry.metric.context.partial"); + case "unsupported" -> ObservabilityMessages.get("observability.telemetry.metric.context.unsupported"); default -> compatibility; - }); + })); } if (StringUtils.hasText(greptimeCompatibility) || StringUtils.hasText(facadeCompatibility)) { segments.add("Greptime=" + defaultText(greptimeCompatibility, "unknown") @@ -1287,10 +1294,10 @@ public class TelemetryIntakeServiceImpl implements TelemetryEvidenceGateway { segments.add("monotonic=" + monotonic); } if ("summary".equals(metricType) && StringUtils.hasText(quantiles)) { - segments.add("Summary quantiles 已保留兼容元信息"); + segments.add(ObservabilityMessages.get("observability.telemetry.metric.context.summary-quantiles")); } if ("histogram".equals(metricType) && StringUtils.hasText(histogramBuckets) && StringUtils.hasText(histogramBounds)) { - segments.add("Histogram buckets/bounds 已保留兼容元信息"); + segments.add(ObservabilityMessages.get("observability.telemetry.metric.context.histogram-buckets")); } if (StringUtils.hasText(reason)) { segments.add(reason); diff --git a/hertzbeat-observability/src/main/resources/observability_en_US.properties b/hertzbeat-observability/src/main/resources/observability_en_US.properties new file mode 100644 index 0000000000..32d90f7f43 --- /dev/null +++ b/hertzbeat-observability/src/main/resources/observability_en_US.properties @@ -0,0 +1,120 @@ +observability.entity.next.review-alerts.title=Review active alerts +observability.entity.next.review-alerts.description={0} active alerts are linked to this entity. Confirm the impact scope and current status first. +observability.entity.next.review-alerts.action=View active alerts +observability.entity.next.complete-owner.title=Assign owner +observability.entity.next.complete-owner.description=This entity has no clear owner yet, so troubleshooting and governance actions lack accountability. +observability.entity.next.complete-owner.action=Assign owner +observability.entity.next.complete-runbook.title=Add runbook +observability.entity.next.complete-runbook.description=Add a runbook or response entry first so alerts can lead directly to recovery. +observability.entity.next.complete-runbook.action=Add runbook +observability.entity.next.bind-monitor.title=Bind monitors +observability.entity.next.bind-monitor.description=The entity exists but has no bound monitor tasks yet, so the current status can only stay at catalog metadata level. +observability.entity.next.bind-monitor.action=Link monitors +observability.entity.next.abnormal-monitors.title=Review abnormal monitors +observability.entity.next.abnormal-monitors.description={0} monitors are abnormal. Confirm which evidence is most useful for deeper investigation. +observability.entity.next.abnormal-monitors.action=View core monitors +observability.entity.next.open-discovery.title=Merge evidence into entity +observability.entity.next.open-discovery.description=There is not enough monitor, identity, or log evidence yet. Return to the discovery workbench to merge evidence first. +observability.entity.next.open-discovery.action=Open discovery workbench +observability.entity.next.inspect-logs.title=Review log clues +observability.entity.next.inspect-logs.description=Log entry points are ready. Check whether OTel resource filters or fallback queries can locate the issue quickly. +observability.entity.next.inspect-logs.action=Review log clues +observability.entity.next.review-relations.title=Add key relations +observability.entity.next.review-relations.description=Critical upstream and downstream relations are incomplete. Add the most important dependency boundaries first. +observability.entity.next.review-relations.action=View relations +observability.entity.next.fallback.title=Continue investigating this entity +observability.entity.next.fallback.description=Catalog ownership and evidence are ready. Continue deeper analysis from logs or monitor status. +observability.entity.next.fallback.action=Review log clues +observability.entity.return-label.fallback=Entity detail +observability.telemetry.metric.entity-monitor-status=Entity monitor status +observability.telemetry.triage.metrics.alert.summary=Active alerts already exist. Confirm whether the monitor plane is still abnormal. +observability.telemetry.triage.metrics.monitor.summary=The monitor plane has abnormal or fluctuating signals. Start with metrics and monitor status. +observability.telemetry.triage.metrics.alert.reason=Active alerts usually mean the impact is clear. Review monitor and alert context first. +observability.telemetry.triage.metrics.monitor.reason=Abnormal monitors often show health changes fastest, making them a good first observation surface. +observability.telemetry.triage.metrics.title=Review monitors first +observability.telemetry.triage.metrics.action=View monitors +observability.telemetry.triage.traces.title=Review traces first +observability.telemetry.triage.traces.summary=Trace-side errors or active calls are present. Inspect call paths and error spans first. +observability.telemetry.triage.traces.reason=Traces help identify which components a request passed through and where the failure happened. +observability.telemetry.triage.traces.action=View traces +observability.telemetry.triage.logs.title=Review logs first +observability.telemetry.triage.logs.summary=Direct log retrieval clues are available. Inspect recent error text and surrounding context first. +observability.telemetry.triage.logs.reason=Logs usually fill in exception details fastest and help confirm concrete errors and impact scope. +observability.telemetry.triage.logs.action=View logs +observability.telemetry.triage.evidence.title=Collect more evidence +observability.telemetry.triage.evidence.summary=There are not enough strong three-signal clues yet. Check intake status or add more runtime data first. +observability.telemetry.triage.evidence.reason=Confirm whether recent metrics, logs, or traces have arrived before continuing the investigation. +observability.telemetry.triage.evidence.action=View evidence +observability.telemetry.metric.context.compatibility=OTLP metrics compatibility: {0} +observability.telemetry.metric.context.supported=supported +observability.telemetry.metric.context.partial=partial support +observability.telemetry.metric.context.unsupported=unsupported +observability.telemetry.metric.context.summary-quantiles=Summary quantiles compatibility metadata is retained. +observability.telemetry.metric.context.histogram-buckets=Histogram buckets and bounds compatibility metadata is retained. +observability.otlp.metric.compatibility.unknown-type=Unknown OTLP metric type. The current HertzBeat facade does not recognize it yet. +observability.otlp.metric.compatibility.histogram.reason=Histogram metrics are written to Greptime, while the current HertzBeat facade keeps representative values and bucket/bounds metadata without full histogram query semantics. +observability.otlp.metric.compatibility.exponential-histogram.reason=The current Greptime OTLP metrics data model does not support ExponentialHistogram. HertzBeat keeps representative values and compatibility metadata. +observability.otlp.metric.compatibility.summary.reason=Summary quantile semantics are limited by the Greptime data model and the current HertzBeat facade. Summary and quantile metadata is retained. +observability.otlp.metric.greptime.reason.supported=The current Greptime OTLP metrics data model supports this type. +observability.otlp.metric.greptime.reason.partial=The current Greptime OTLP metrics data model preserves this type only partially. +observability.otlp.metric.greptime.reason.unsupported=The current Greptime OTLP metrics data model does not support this type. +observability.otlp.metric.facade.reason.supported=The current HertzBeat facade can consume this type directly. +observability.otlp.metric.facade.reason.partial=The current HertzBeat facade keeps only representative values and compatibility metadata. +observability.otlp.metric.facade.reason.unsupported=The current HertzBeat facade does not support this type. +observability.otlp.overview.event.metrics.title=Metrics received +observability.otlp.overview.event.metrics.copy=Recent metrics data received +observability.otlp.overview.metrics.active=Recent queryable metrics data has been received. +observability.otlp.overview.metrics.inactive=No metrics data has arrived in the last 24 hours. +observability.otlp.overview.logs.active=Logs data has arrived in the last 24 hours. +observability.otlp.overview.logs.inactive=No logs data has arrived in the last 24 hours. +observability.otlp.overview.traces.active=Trace data has arrived in the last 24 hours. +observability.otlp.overview.traces.inactive=No trace data has arrived in the last 24 hours. +observability.otlp.overview.metrics.mode.mixed=OTLP + entity monitors +observability.otlp.overview.metrics.mode.monitor=Entity monitors +observability.otlp.readiness.collector.title=Collector cluster +observability.otlp.readiness.collector.unregistered=No Collector is registered. +observability.otlp.readiness.collector.deploy=Deploy a Collector or use the built-in intake endpoint first. +observability.otlp.readiness.collector.online={0} / {1} online +observability.otlp.readiness.collector.accepting=Collector nodes can receive tasks. +observability.otlp.readiness.collector.offline={0} collector nodes are offline. +observability.otlp.readiness.collector.all-offline=All collector nodes are offline. +observability.otlp.readiness.storage.title=History storage +observability.otlp.readiness.storage.disabled=History storage is not enabled. +observability.otlp.readiness.storage.check-config=Check the history storage configuration. +observability.otlp.readiness.storage.available={0} / {1} available +observability.otlp.readiness.storage.reader-available=HistoryDataReader is available. +observability.otlp.readiness.storage.partial=Some history storage backends are unavailable. +observability.otlp.readiness.query.title=Query service +observability.otlp.readiness.query.available=Metrics, logs, and traces queries are available. +observability.otlp.readiness.query.promql-history=PromQL and history queries are available. +observability.otlp.readiness.query.partial=Some query capabilities are available. +observability.otlp.readiness.query.promql-only=PromQL is available. History queries need checking. +observability.otlp.readiness.query.history-only=History queries are available. PromQL needs checking. +observability.otlp.readiness.query.unavailable=Query service is unavailable. +observability.otlp.readiness.query.check-config=Check the PromQL and history query configuration. +observability.otlp.readiness.greptime.disabled=GreptimeDB is not enabled. +observability.otlp.readiness.greptime.other-storage=Another history storage backend is in use, or storage is not configured yet. +observability.otlp.readiness.greptime.sql-not-ready=GreptimeDB is enabled, but the SQL executor is not ready. +observability.otlp.readiness.greptime.check-http=Check the GreptimeDB HTTP configuration. +observability.otlp.readiness.greptime.sql-ok=SQL self-check passed. +observability.otlp.readiness.greptime.select-ok=SELECT 1 succeeded. +observability.otlp.readiness.greptime.sql-failed=SQL self-check failed. +observability.otlp.readiness.greptime.check-connection=Check the GreptimeDB connection. +observability.otlp.guide.snippet.python.http.comment=All signals are sent to the HertzBeat OTLP HTTP endpoint: +observability.otlp.guide.snippet.python.grpc.comment=All signals are sent to the HertzBeat OTLP gRPC endpoint: +observability.otlp.guide.metrics.http.description=Send metrics data to this OTLP HTTP endpoint. After data arrives, continue from monitors and entity details. +observability.otlp.guide.metrics.http.note=If monitor tasks are already configured, the data continues to appear in the corresponding monitors. +observability.otlp.guide.logs.http.description=Send logs data to this OTLP HTTP endpoint. After data arrives, review it from log management. +observability.otlp.guide.logs.http.note=Use an intake token for authentication. +observability.otlp.guide.traces.http.description=Send trace data to this OTLP HTTP endpoint. After data arrives, review it from trace management. +observability.otlp.guide.traces.http.note=The system tries to associate data with entities from service information automatically. +observability.otlp.guide.metrics.grpc.description=Send metrics data to this OTLP gRPC endpoint. This fits services already using an OpenTelemetry Collector or language SDK. +observability.otlp.guide.metrics.grpc.note=After data arrives, continue from monitors and entity details. +observability.otlp.guide.logs.grpc.description=Send logs data to this OTLP gRPC endpoint. After data arrives, review it from log management. +observability.otlp.guide.logs.grpc.note=Use the Authorization Bearer intake token for authentication. +observability.otlp.guide.traces.grpc.description=Send trace data to this OTLP gRPC endpoint. After data arrives, review it from trace management. +observability.otlp.guide.traces.grpc.note=The system tries to associate data with entities from service information automatically. +observability.otlp.guide.snippet.java-env=Java environment variables +observability.otlp.guide.snippet.python-resource=Python resource attributes +observability.otlp.metrics-console.no-context=Missing service context for building the OTLP metrics query. +observability.otlp.metrics-console.promql-unavailable=No available PromQL metrics query executor is configured in the current environment. diff --git a/hertzbeat-observability/src/main/resources/observability_zh_CN.properties b/hertzbeat-observability/src/main/resources/observability_zh_CN.properties new file mode 100644 index 0000000000..a07bf338e6 --- /dev/null +++ b/hertzbeat-observability/src/main/resources/observability_zh_CN.properties @@ -0,0 +1,120 @@ +observability.entity.next.review-alerts.title=处理活跃告警 +observability.entity.next.review-alerts.description={0} 条活跃告警已经关联到当前实体,先确认影响范围和当前状态。 +observability.entity.next.review-alerts.action=查看活跃告警 +observability.entity.next.complete-owner.title=补负责人 +observability.entity.next.complete-owner.description=当前实体还没有明确负责人,排障和治理动作缺少责任归属。 +observability.entity.next.complete-owner.action=补负责人 +observability.entity.next.complete-runbook.title=补处置手册 +observability.entity.next.complete-runbook.description=先补一条 runbook 或处置入口,告警后才能直接推动恢复。 +observability.entity.next.complete-runbook.action=补处置手册 +observability.entity.next.bind-monitor.title=补绑监控 +observability.entity.next.bind-monitor.description=实体已经存在,但还没有绑定任何监控任务,当前状态只能停留在目录元数据层。 +observability.entity.next.bind-monitor.action=关联监控 +observability.entity.next.abnormal-monitors.title=查看异常监控 +observability.entity.next.abnormal-monitors.description={0} 个监控当前异常,建议先确认哪个证据最值得继续排查。 +observability.entity.next.abnormal-monitors.action=查看核心监控 +observability.entity.next.open-discovery.title=归并证据到实体 +observability.entity.next.open-discovery.description=当前还没有形成足够的监控、身份或日志线索,建议先回到发现工作台归并证据。 +observability.entity.next.open-discovery.action=打开发现工作台 +observability.entity.next.inspect-logs.title=查看日志线索 +observability.entity.next.inspect-logs.description=日志入口已经具备,先确认 OTel resource 或 fallback 查询是否能快速定位异常。 +observability.entity.next.inspect-logs.action=查看日志线索 +observability.entity.next.review-relations.title=补关键关系 +observability.entity.next.review-relations.description=关键上下游关系还不完整,建议先补齐最关键的依赖边界。 +observability.entity.next.review-relations.action=查看关系 +observability.entity.next.fallback.title=继续排查当前实体 +observability.entity.next.fallback.description=目录归属和证据都已经具备,可以直接从日志或监控继续深入分析。 +observability.entity.next.fallback.action=查看日志线索 +observability.entity.return-label.fallback=实体详情 +observability.telemetry.metric.entity-monitor-status=实体监控状态 +observability.telemetry.triage.metrics.alert.summary=当前已有活跃告警,建议先确认监控面是否正在持续异常。 +observability.telemetry.triage.metrics.monitor.summary=当前监控面已经出现异常或波动,建议先从指标和监控状态入手。 +observability.telemetry.triage.metrics.alert.reason=活跃告警通常意味着影响已经明确,可先查看监控和告警上下文。 +observability.telemetry.triage.metrics.monitor.reason=异常监控最能快速反映健康度变化,适合作为第一观察面。 +observability.telemetry.triage.metrics.title=优先查看监控 +observability.telemetry.triage.metrics.action=查看监控 +observability.telemetry.triage.traces.title=优先查看链路 +observability.telemetry.triage.traces.summary=当前链路侧已经出现错误或活跃调用,适合先查看调用路径和错误跨度。 +observability.telemetry.triage.traces.reason=链路最容易帮助定位请求经过了哪些组件,以及失败发生在什么位置。 +observability.telemetry.triage.traces.action=查看链路 +observability.telemetry.triage.logs.title=优先查看日志 +observability.telemetry.triage.logs.summary=当前已有可直接使用的日志检索线索,适合先查看最近错误文本和上下文。 +observability.telemetry.triage.logs.reason=日志通常能最快补全异常细节,帮助确认具体报错和影响范围。 +observability.telemetry.triage.logs.action=查看日志 +observability.telemetry.triage.evidence.title=继续补充证据 +observability.telemetry.triage.evidence.summary=当前还没有足够强的三信号线索,建议先确认接入状态或补充更多运行数据。 +observability.telemetry.triage.evidence.reason=先确认最近是否已经收到指标、日志或链路数据,再继续定位。 +observability.telemetry.triage.evidence.action=查看证据 +observability.telemetry.metric.context.compatibility=OTLP metrics 兼容性:{0} +observability.telemetry.metric.context.supported=支持 +observability.telemetry.metric.context.partial=部分支持 +observability.telemetry.metric.context.unsupported=不支持 +observability.telemetry.metric.context.summary-quantiles=Summary quantiles 已保留兼容元信息 +observability.telemetry.metric.context.histogram-buckets=Histogram buckets/bounds 已保留兼容元信息 +observability.otlp.metric.compatibility.unknown-type=未知 OTLP metric 类型,当前未被 HertzBeat facade 识别。 +observability.otlp.metric.compatibility.histogram.reason=Histogram 指标已写入 Greptime,但 HertzBeat 当前 facade 仅保留代表值与 bucket/bounds 元信息,未提供完整 histogram 查询语义。 +observability.otlp.metric.compatibility.exponential-histogram.reason=Greptime 当前 OTLP metrics 数据模型不支持 ExponentialHistogram;HertzBeat 仅保留代表值与兼容性元信息。 +observability.otlp.metric.compatibility.summary.reason=Summary quantiles 语义受 Greptime 数据模型与当前 HertzBeat facade 限制,当前仅保留 summary/quantiles 元信息。 +observability.otlp.metric.greptime.reason.supported=Greptime 当前 OTLP metrics 数据模型支持该类型。 +observability.otlp.metric.greptime.reason.partial=Greptime 当前 OTLP metrics 数据模型仅部分保留该类型语义。 +observability.otlp.metric.greptime.reason.unsupported=Greptime 当前 OTLP metrics 数据模型不支持该类型。 +observability.otlp.metric.facade.reason.supported=HertzBeat 当前 facade 可直接消费该类型。 +observability.otlp.metric.facade.reason.partial=HertzBeat 当前 facade 仅保留代表值与兼容元信息。 +observability.otlp.metric.facade.reason.unsupported=HertzBeat 当前 facade 不支持该类型。 +observability.otlp.overview.event.metrics.title=指标已接入 +observability.otlp.overview.event.metrics.copy=最近已收到指标数据 +observability.otlp.overview.metrics.active=最近已收到可查看的指标数据 +observability.otlp.overview.metrics.inactive=最近 24 小时还没有指标数据 +observability.otlp.overview.logs.active=最近 24 小时已收到日志数据 +observability.otlp.overview.logs.inactive=最近 24 小时还没有日志数据 +observability.otlp.overview.traces.active=最近 24 小时已收到链路数据 +observability.otlp.overview.traces.inactive=最近 24 小时还没有链路数据 +observability.otlp.overview.metrics.mode.mixed=OTLP + 实体监控 +observability.otlp.overview.metrics.mode.monitor=实体监控 +observability.otlp.readiness.collector.title=Collector 集群 +observability.otlp.readiness.collector.unregistered=未注册 Collector +observability.otlp.readiness.collector.deploy=先部署 Collector 或使用内置采集入口 +observability.otlp.readiness.collector.online={0} / {1} 在线 +observability.otlp.readiness.collector.accepting=采集节点可接收任务 +observability.otlp.readiness.collector.offline={0} 个采集节点离线 +observability.otlp.readiness.collector.all-offline=所有采集节点离线 +observability.otlp.readiness.storage.title=历史存储 +observability.otlp.readiness.storage.disabled=未启用历史存储 +observability.otlp.readiness.storage.check-config=检查历史存储配置 +observability.otlp.readiness.storage.available={0} / {1} 可用 +observability.otlp.readiness.storage.reader-available=HistoryDataReader 可用 +observability.otlp.readiness.storage.partial=部分历史存储不可用 +observability.otlp.readiness.query.title=查询服务 +observability.otlp.readiness.query.available=指标、日志和链路查询可用 +observability.otlp.readiness.query.promql-history=PromQL 与历史查询可用 +observability.otlp.readiness.query.partial=部分查询能力可用 +observability.otlp.readiness.query.promql-only=PromQL 可用,历史查询待检查 +observability.otlp.readiness.query.history-only=历史查询可用,PromQL 待检查 +observability.otlp.readiness.query.unavailable=查询服务不可用 +observability.otlp.readiness.query.check-config=检查 PromQL 与历史查询配置 +observability.otlp.readiness.greptime.disabled=未启用 GreptimeDB +observability.otlp.readiness.greptime.other-storage=当前使用其他历史存储或尚未配置 +observability.otlp.readiness.greptime.sql-not-ready=GreptimeDB 已启用,SQL 执行器未就绪 +observability.otlp.readiness.greptime.check-http=检查 GreptimeDB HTTP 配置 +observability.otlp.readiness.greptime.sql-ok=SQL 自检通过 +observability.otlp.readiness.greptime.select-ok=SELECT 1 成功 +observability.otlp.readiness.greptime.sql-failed=SQL 自检失败 +observability.otlp.readiness.greptime.check-connection=检查 GreptimeDB 连接 +observability.otlp.guide.snippet.python.http.comment=所有 signals 统一发送到 HertzBeat OTLP HTTP 入口: +observability.otlp.guide.snippet.python.grpc.comment=所有 signals 统一发送到 HertzBeat OTLP gRPC 入口: +observability.otlp.guide.metrics.http.description=把指标数据发送到这个 OTLP HTTP 地址。收到数据后,可直接在监控和实体详情中继续查看。 +observability.otlp.guide.metrics.http.note=如果已经配置了监控任务,数据会继续显示在对应的监控项中。 +observability.otlp.guide.logs.http.description=把日志数据发送到这个 OTLP HTTP 地址。收到数据后,可直接前往日志管理查看。 +observability.otlp.guide.logs.http.note=使用接入令牌即可完成认证。 +observability.otlp.guide.traces.http.description=把链路数据发送到这个 OTLP HTTP 地址。收到数据后,可直接前往链路管理查看。 +observability.otlp.guide.traces.http.note=系统会根据服务信息自动尝试关联到实体。 +observability.otlp.guide.metrics.grpc.description=把指标数据发送到这个 OTLP gRPC 地址,适合已经使用 OpenTelemetry Collector 或语言 SDK 的服务。 +observability.otlp.guide.metrics.grpc.note=收到数据后,可直接在监控和实体详情中继续查看。 +observability.otlp.guide.logs.grpc.description=把日志数据发送到这个 OTLP gRPC 地址。收到数据后,可直接前往日志管理查看。 +observability.otlp.guide.logs.grpc.note=使用 Authorization Bearer 接入令牌即可完成认证。 +observability.otlp.guide.traces.grpc.description=把链路数据发送到这个 OTLP gRPC 地址。收到数据后,可直接前往链路管理查看。 +observability.otlp.guide.traces.grpc.note=系统会根据服务信息自动尝试关联到实体。 +observability.otlp.guide.snippet.java-env=Java 环境变量 +observability.otlp.guide.snippet.python-resource=Python 资源属性 +observability.otlp.metrics-console.no-context=缺少可用于构建 OTLP 指标查询的服务上下文。 +observability.otlp.metrics-console.promql-unavailable=当前环境未配置可用的 PromQL 指标查询执行器。 diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java index f7b11a189f..88fa4e03b0 100644 --- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java +++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java @@ -74,7 +74,8 @@ class OtlpIngestionControllerTest { ); overview.setReadinessChecks(List.of( new OtlpIngestionOverviewDto.ReadinessCheck( - "collector", "Collector 集群", "success", "1 / 1 在线", "采集节点可接收任务", 1_710_000_000_100L) + "collector", "Collector cluster", "success", "1 / 1 online", + "Collector nodes can receive tasks.", 1_710_000_000_100L) )); when(otlpIngestionWorkspaceService.getOverview()).thenReturn(overview); @@ -85,7 +86,7 @@ class OtlpIngestionControllerTest { .andExpect(jsonPath("$.data.boundEntityCount").value(4)) .andExpect(jsonPath("$.data.logs.totalCount").value(5)) .andExpect(jsonPath("$.data.readinessChecks[0].key").value("collector")) - .andExpect(jsonPath("$.data.readinessChecks[0].summary").value("1 / 1 在线")); + .andExpect(jsonPath("$.data.readinessChecks[0].summary").value("1 / 1 online")); verify(otlpIngestionWorkspaceService).getOverview(); } diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionServiceSourceLocalizationTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionServiceSourceLocalizationTest.java new file mode 100644 index 0000000000..ae7576a549 --- /dev/null +++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionServiceSourceLocalizationTest.java @@ -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. + */ + +package org.apache.hertzbeat.observability.ingestion.service.impl; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + +class OtlpIngestionServiceSourceLocalizationTest { + + private static final List<Path> PRODUCTION_SOURCES = List.of( + Path.of("src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/" + + "OtlpGrpcIngestionServiceImpl.java"), + Path.of("src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/" + + "OtlpIngestionWorkspaceServiceImpl.java") + ); + private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{Script=Han}"); + + @Test + void productionSourcesShouldNotContainHanScriptLiterals() throws IOException { + for (Path productionSource : PRODUCTION_SOURCES) { + String source = Files.readString(productionSource); + assertFalse(HAN_SCRIPT.matcher(source).find(), + () -> "source file must not contain Han-script literals: " + productionSource); + } + } +} diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java index 4f3309d874..ab48464a6f 100644 --- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java +++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java @@ -36,6 +36,7 @@ import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import org.apache.hertzbeat.common.entity.dto.query.DatasourceQueryData; @@ -58,6 +59,7 @@ import org.apache.hertzbeat.warehouse.repository.MetricQueryRepository; import org.apache.hertzbeat.warehouse.db.GreptimeSqlQueryExecutor; import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader; import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -89,8 +91,12 @@ class OtlpIngestionWorkspaceServiceImplTest { private ObservabilitySignalIntakeGateway observabilitySignalIntakeGateway; + private Locale previousLocale; + @BeforeEach void setUp() { + previousLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); observabilitySignalIntakeGateway = new InMemoryObservabilitySignalIntakeGateway(); otlpIngestionWorkspaceService = new OtlpIngestionWorkspaceServiceImpl( entityTraceQueryService, @@ -104,6 +110,11 @@ class OtlpIngestionWorkspaceServiceImplTest { ); } + @AfterEach + void restoreLocale() { + Locale.setDefault(previousLocale); + } + private void stubRecentLogs(LogEntry... logs) { when(logQueryRepository.queryRecentLogs(anyLong(), anyLong(), eq(20))).thenReturn(List.of(logs)); } @@ -208,16 +219,16 @@ class OtlpIngestionWorkspaceServiceImplTest { overview.getReadinessChecks().stream().map(OtlpIngestionOverviewDto.ReadinessCheck::getKey).toList()); assertTrue(overview.getReadinessChecks().stream().anyMatch(check -> "collector".equals(check.getKey()) && "warning".equals(check.getStatus()) - && check.getSummary().contains("2 / 3 在线"))); + && check.getSummary().contains("2 / 3 online"))); assertTrue(overview.getReadinessChecks().stream().anyMatch(check -> "storage".equals(check.getKey()) && "success".equals(check.getStatus()) - && check.getSummary().contains("1 / 1 可用"))); + && check.getSummary().contains("1 / 1 available"))); assertTrue(overview.getReadinessChecks().stream().anyMatch(check -> "query".equals(check.getKey()) && "success".equals(check.getStatus()) - && check.getSummary().contains("指标、日志和链路查询可用"))); + && check.getSummary().contains("Metrics, logs, and traces queries are available."))); assertTrue(overview.getReadinessChecks().stream().anyMatch(check -> "greptime".equals(check.getKey()) && "success".equals(check.getStatus()) - && check.getSummary().contains("SQL 自检通过"))); + && check.getSummary().contains("SQL self-check passed."))); } @Test @@ -280,7 +291,7 @@ class OtlpIngestionWorkspaceServiceImplTest { && snippet.getContent().contains("endpoint: demo.hertzbeat.apache.org:4317") && snippet.getContent().contains("Authorization: \"Bearer <api-token>\""))); assertTrue(guide.getSignals().stream().filter(signal -> "grpc".equals(signal.getProtocol())) - .allMatch(signal -> signal.getNote() == null || !signal.getNote().contains("登录 token"))); + .allMatch(signal -> signal.getNote() == null || !signal.getNote().contains("login token"))); assertFalse(guide.getSnippets().isEmpty()); } diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java index 9fbb86ddb3..e2fcb981ff 100644 --- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java +++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImplTest.java @@ -25,10 +25,15 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.LocalDateTime; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.ResourceBundle; +import java.util.regex.Pattern; import org.apache.hertzbeat.common.constants.CommonConstants; import org.apache.hertzbeat.common.entity.alerter.SingleAlert; import org.apache.hertzbeat.common.entity.manager.EntityIdentity; @@ -59,16 +64,45 @@ import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceQueryHintD import org.apache.hertzbeat.common.observability.dto.trace.EntityTraceSummaryDto; import org.apache.hertzbeat.common.observability.model.ObservedEntityContext; import org.apache.hertzbeat.observability.traces.service.EntityTraceQueryService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; class EntityObservabilityGatewayImplTest { + private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{Script=Han}"); + private static final Path ENTITY_OBSERVABILITY_GATEWAY_IMPL = Path.of( + "src/main/java/org/apache/hertzbeat/observability/shared/service/impl/EntityObservabilityGatewayImpl.java"); + private final TelemetryIntakeServiceImpl telemetryIntakeService = Mockito.mock(TelemetryIntakeServiceImpl.class); private final EntityTraceQueryService entityTraceQueryService = Mockito.mock(EntityTraceQueryService.class); private final EntityObservabilityGatewayImpl gateway = new EntityObservabilityGatewayImpl(telemetryIntakeService, entityTraceQueryService); + private Locale previousLocale; + + @BeforeEach + void useEnglishCatalog() { + previousLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); + ResourceBundle.clearCache(); + } + + @AfterEach + void restoreLocale() { + Locale.setDefault(previousLocale); + ResourceBundle.clearCache(); + } + + @Test + void sourceShouldNotContainHanScriptUserFacingCopy() throws Exception { + String source = Files.readString(ENTITY_OBSERVABILITY_GATEWAY_IMPL); + + assertFalse(HAN_SCRIPT.matcher(source).find(), + "User-facing entity observability copy belongs in locale resources, not Java source"); + } + @Test void resolveEntityTraceSummaryShouldFallbackToQueryServiceWhenInactive() { ObservedEntityContext entityContext = ObservedEntityContext.from(null, Collections.emptyList()); @@ -132,7 +166,7 @@ class EntityObservabilityGatewayImplTest { EntityUnifiedEvidenceSummary unifiedEvidenceSummary = new EntityUnifiedEvidenceSummary(3, true, true, true, 1L, 1, 1, 12L, List.of("metrics", "logs", "traces")); EntityTriageRecommendation triageRecommendation = - new EntityTriageRecommendation("focus_logs", "logs", "处理日志", "查看日志", "high", "进入日志", 13L); + new EntityTriageRecommendation("focus_logs", "logs", "Handle logs", "Review logs", "high", "Open logs", 13L); when(telemetryIntakeService.buildMetricEvidence(any(), any(), any())).thenReturn(metricEvidence); when(telemetryIntakeService.buildLogEvidence(any(), any(), any())).thenReturn(logEvidence); @@ -161,7 +195,7 @@ class EntityObservabilityGatewayImplTest { @Test void enrichEntityLogQueryHintsShouldPreferEvidenceAndTraceFallbacks() { EntityLogQueryHint originalHint = new EntityLogQueryHint(); - originalHint.setTitle("原始日志"); + originalHint.setTitle("Original logs"); originalHint.setResourceFilters(Map.of()); originalHint.setSearchTerms(List.of()); @@ -195,7 +229,7 @@ class EntityObservabilityGatewayImplTest { assertEquals(1, enriched.size()); EntityLogQueryHint first = enriched.getFirst(); - assertEquals("原始日志", first.getTitle()); + assertEquals("Original logs", first.getTitle()); assertEquals(Map.of("service.name", "checkout"), first.getResourceFilters()); assertEquals(List.of("checkout", "error"), first.getSearchTerms()); assertEquals("trace-1", first.getTraceId()); @@ -211,7 +245,7 @@ class EntityObservabilityGatewayImplTest { @Test void buildEntityLogSummaryShouldUsePreferredHintAsSummarySource() { EntityLogQueryHint hint = new EntityLogQueryHint( - "日志入口", + "Log entry", Map.of("service.name", "checkout"), List.of("checkout", "error"), "trace-1", @@ -226,8 +260,8 @@ class EntityObservabilityGatewayImplTest { EntityLogSummaryInfo summary = gateway.buildEntityLogSummary(List.of(hint)); assertEquals(1, summary.getHintCount()); - assertEquals("日志入口", summary.getPreferredQueryType()); - assertEquals("日志入口", summary.getPreferredQueryTitle()); + assertEquals("Log entry", summary.getPreferredQueryType()); + assertEquals("Log entry", summary.getPreferredQueryTitle()); assertEquals(Map.of("service.name", "checkout"), summary.getPreferredResourceFilters()); assertEquals(List.of("checkout", "error"), summary.getPreferredSearchTerms()); assertEquals("checkout", summary.getFallbackSearchTerm()); @@ -494,7 +528,7 @@ class EntityObservabilityGatewayImplTest { entity.setDisplayName(null); assertEquals("checkout-service", gateway.buildEntityReturnLabel(entityContext)); - assertEquals("实体详情", gateway.buildEntityReturnLabel(ObservedEntityContext.from(null, Collections.emptyList()))); + assertEquals("Entity detail", gateway.buildEntityReturnLabel(ObservedEntityContext.from(null, Collections.emptyList()))); } @Test @@ -654,7 +688,7 @@ class EntityObservabilityGatewayImplTest { void buildEntityDiscoveryHandoffShouldCarryEntityContextAndPreferAlertQuery() { EntityResponseHandoffInfo handoff = gateway.buildEntityDiscoveryHandoff( "/entities/1", - "结账服务", + "Checkout service", "team-a", "checkout", "prod", @@ -665,7 +699,7 @@ class EntityObservabilityGatewayImplTest { ); assertEquals("/entities/1", handoff.getReturnTo()); - assertEquals("结账服务", handoff.getReturnLabel()); + assertEquals("Checkout service", handoff.getReturnLabel()); assertEquals("alert-token", handoff.getQuery()); assertEquals("team-a", handoff.getOwner()); assertEquals("checkout", handoff.getSystem()); @@ -676,13 +710,13 @@ class EntityObservabilityGatewayImplTest { @Test void buildEntityEditorHandoffShouldSelectFocusByReadinessPriority() { EntityResponseHandoffInfo ownershipHandoff = - gateway.buildEntityEditorHandoff("/entities/1", "结账服务", false, true, true, true); + gateway.buildEntityEditorHandoff("/entities/1", "Checkout service", false, true, true, true); EntityResponseHandoffInfo relationHandoff = - gateway.buildEntityEditorHandoff("/entities/1", "结账服务", true, true, false, true); + gateway.buildEntityEditorHandoff("/entities/1", "Checkout service", true, true, false, true); EntityResponseHandoffInfo monitorHandoff = - gateway.buildEntityEditorHandoff("/entities/1", "结账服务", true, true, true, false); + gateway.buildEntityEditorHandoff("/entities/1", "Checkout service", true, true, true, false); EntityResponseHandoffInfo defaultHandoff = - gateway.buildEntityEditorHandoff("/entities/1", "结账服务", true, true, true, true); + gateway.buildEntityEditorHandoff("/entities/1", "Checkout service", true, true, true, true); assertEquals("ownership", ownershipHandoff.getFocus()); assertEquals("relations", relationHandoff.getFocus()); @@ -694,7 +728,7 @@ class EntityObservabilityGatewayImplTest { void buildEntityResponseHandoffsShouldAssembleAllTargetsFromRequest() { ObserveEntity entity = new ObserveEntity(); entity.setName("checkout"); - entity.setDisplayName("结账服务"); + entity.setDisplayName("Checkout service"); EntityIdentity identity = EntityIdentity.builder() .identityKey("service.name") .identityValue("checkout") @@ -704,7 +738,7 @@ class EntityObservabilityGatewayImplTest { EntityTraceSummaryDto traceSummary = new EntityTraceSummaryDto(1, 1, 20L, true, "trace-1"); EntityResponseHandoffsRequest request = new EntityResponseHandoffsRequest( "/entities/1", - "结账服务", + "Checkout service", "team-a", "checkout-system", "prod", @@ -758,7 +792,7 @@ class EntityObservabilityGatewayImplTest { ); EntityResponseHandoffsRequest request = new EntityResponseHandoffsRequest( "/entities/1", - "结账服务", + "Checkout service", "team-a", "checkout-system", "prod", diff --git a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImplTest.java b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImplTest.java index e60f26de0a..0ba7c3bec9 100644 --- a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImplTest.java +++ b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImplTest.java @@ -24,9 +24,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.ResourceBundle; +import java.util.regex.Pattern; import org.apache.hertzbeat.common.entity.log.LogEntry; import org.apache.hertzbeat.common.entity.manager.EntityIdentity; import org.apache.hertzbeat.common.entity.manager.ObserveEntity; @@ -44,6 +49,8 @@ import org.apache.hertzbeat.common.observability.model.CodeNavigationHint; import org.apache.hertzbeat.common.observability.model.ObservedEntityContext; import org.apache.hertzbeat.common.util.JsonUtil; import org.apache.hertzbeat.warehouse.repository.LogQueryRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -51,10 +58,37 @@ import org.mockito.Mockito; class TelemetryIntakeServiceImplTest { + private static final Pattern HAN_SCRIPT = Pattern.compile("\\p{Script=Han}"); + private static final Path TELEMETRY_INTAKE_SERVICE_IMPL = Path.of( + "src/main/java/org/apache/hertzbeat/observability/shared/service/impl/TelemetryIntakeServiceImpl.java"); + private final LogQueryRepository logQueryRepository = Mockito.mock(LogQueryRepository.class); private final TelemetryIntakeServiceImpl telemetryIntakeService = new TelemetryIntakeServiceImpl(logQueryRepository); + private Locale previousLocale; + + @BeforeEach + void useEnglishCatalog() { + previousLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); + ResourceBundle.clearCache(); + } + + @AfterEach + void restoreLocale() { + Locale.setDefault(previousLocale); + ResourceBundle.clearCache(); + } + + @Test + void sourceShouldNotContainHanScriptUserFacingCopy() throws Exception { + String source = Files.readString(TELEMETRY_INTAKE_SERVICE_IMPL); + + assertFalse(HAN_SCRIPT.matcher(source).find(), + "User-facing telemetry intake copy belongs in locale resources, not Java source"); + } + @Test void buildCodeNavigationHintUsesEntityCodeLocations() { ObservedEntityContext entityContext = buildObservedEntityContextWithCodeLocations("https://github.com/apache/hertzbeat.git"); @@ -68,7 +102,7 @@ class TelemetryIntakeServiceImplTest { "code.filepath", "hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/ObserveEntityServiceImpl.java" ), List.of("localOtlpTraceIngest"), - "查看代码" + "View code" ); assertNotNull(hint); @@ -657,7 +691,7 @@ class TelemetryIntakeServiceImplTest { "instance", "e2e", "otlp.metric.compatibility", "partial", "otlp.metric.compatibility.reason", - "Summary quantiles 当前仅作为兼容元信息保留,尚未作为一等查询语义暴露。", + "Summary quantiles are retained as compatibility metadata only.", "otlp.metric.greptime.compatibility", "partial", "otlp.metric.facade.compatibility", "partial", "otlp.metric.summary.quantiles", @@ -708,7 +742,7 @@ class TelemetryIntakeServiceImplTest { assertEquals("partial", metricEvidence.getAttributes().get("otlp.metric.compatibility")); assertTrue(metricEvidence.getAttributes().get("otlp.metric.summary.quantiles").contains("\"quantile\":0.95")); assertNotNull(metricEvidence.getOtelContext()); - assertTrue(metricEvidence.getOtelContext().contains("部分支持")); + assertTrue(metricEvidence.getOtelContext().contains("partial support")); assertTrue(metricEvidence.getOtelContext().contains("Summary quantiles")); } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
