This is an automated email from the ASF dual-hosted git repository. tomsun28 pushed a commit to branch 2.0.0 in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
commit 9d53fd7be2b1ede4a3ed659c6d312c1c4ed4f24f Author: tomsun28 <[email protected]> AuthorDate: Fri Oct 9 13:21:49 2026 +0800 fix(merge): port remaining master bugfixes and hardening lost in master->2.0.0 sync Backend: - AlarmGroupReduce: port #4316 retention semantics (firing members stay cached until emitted resolved; never emit a resolved group while any member still fires and #4361 (fall back to default repeat interval when the converge rule was deleted) — the snapshot-flush model reintroduced the resolved-flap bug - NoticeConfigController: restore POST /api/notice/template/preview using NoticeTemplateRenderer (#4338) - NoticeReceiver contract: add ntfy (type 15) and wpush (type 16) fields — the strict @JsonAnySetter was 400-rejecting payloads carrying them even though both notify handlers still exist - SmsLocalSmsClientImpl: use SmsAlertFields (NPE-safe content fallback) CI/CD + packaging: - nightly-build + hybrid-collector-release: pin docker/graalvm actions to the same ASF-allowlisted SHAs used elsewhere (master #4356/#4193) - docker-compose files: re-apply localhost port binding (#4258), greptime v1.1.3 pin (#4244), 14317 OTLP convention; keep apache/hertzbeat:2.0.0 - script/docker/collector/Dockerfile: restore mkdir -p /var/run/sshd (#4143) - application-test.yml: drop stale unisms block (UniSMS removed) Tests (ported/adapted from master): - AlarmGroupReduceTest: 3 convergence regressions (#4316/#4361) - NoticeConfigControllerTest: template preview ok/invalid - ChatClientProviderServiceImplTest: config-change cache refresh + security-context tool callback wrapping (#4230) - GreptimeSqlQueryExecutorTest: form-urlencoded request body (#4223) - AlertDefineJsonImExportServiceTest: queryLanguage round-trip (#4256) EOF ) --- .github/workflows/hybrid-collector-release.yml | 14 +-- .github/workflows/nightly-build.yml | 10 +- .../impl/ChatClientProviderServiceImplTest.java | 95 +++++++++++++++ .../alert/controller/NoticeConfigController.java | 20 ++++ .../hertzbeat/alert/dto/NoticeReceiverOptions.java | 8 ++ .../hertzbeat/alert/dto/NoticeReceiverRequest.java | 6 + .../hertzbeat/alert/reduce/AlarmGroupReduce.java | 23 +++- .../service/NoticeReceiverContractMapper.java | 30 ++++- .../alert/service/impl/SmsLocalSmsClientImpl.java | 7 +- .../controller/NoticeConfigControllerTest.java | 41 +++++++ .../alert/reduce/AlarmGroupReduceTest.java | 132 +++++++++++++++++---- .../AlertDefineJsonImExportServiceTest.java | 3 + .../src/main/resources/application-test.yml | 8 -- .../warehouse/db/GreptimeSqlQueryExecutorTest.java | 33 ++++++ .../hertzbeat-mysql-iotdb/docker-compose.yaml | 14 ++- .../hertzbeat-mysql-tdengine/docker-compose.yaml | 12 +- .../docker-compose.yaml | 12 +- .../docker-compose.yaml | 29 +++-- .../docker-compose.yaml | 18 ++- .../docker-compose.yaml | 18 ++- script/docker/collector/Dockerfile | 2 +- 21 files changed, 448 insertions(+), 87 deletions(-) diff --git a/.github/workflows/hybrid-collector-release.yml b/.github/workflows/hybrid-collector-release.yml index dd456138a0..a455481ae4 100644 --- a/.github/workflows/hybrid-collector-release.yml +++ b/.github/workflows/hybrid-collector-release.yml @@ -56,7 +56,7 @@ jobs: with: go-version-file: hertzbeat-otel-runtime/go.mod cache-dependency-path: hertzbeat-otel-runtime/go.mod - - uses: graalvm/setup-graalvm@v1 + - uses: graalvm/setup-graalvm@186d0493a2df5eb62df5ecc498883d18fd58c303 # v1.6.2 with: java-version: 25 distribution: graalvm @@ -103,7 +103,7 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - uses: graalvm/setup-graalvm@v1 + - uses: graalvm/setup-graalvm@186d0493a2df5eb62df5ecc498883d18fd58c303 # v1.6.2 with: java-version: 25 distribution: graalvm @@ -140,7 +140,7 @@ jobs: with: go-version-file: hertzbeat-otel-runtime/go.mod cache-dependency-path: hertzbeat-otel-runtime/go.mod - - uses: graalvm/setup-graalvm@v1 + - uses: graalvm/setup-graalvm@186d0493a2df5eb62df5ecc498883d18fd58c303 # v1.6.2 with: java-version: 25 distribution: graalvm @@ -182,10 +182,10 @@ jobs: merge-multiple: true - name: Prepare verified native container inputs run: ./script/ci/prepare-hybrid-collector-native-container-context.sh dist target/native-container-context - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Build runnable amd64 native image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: target/native-container-context file: script/docker/collector/Dockerfile.native @@ -195,7 +195,7 @@ jobs: - name: Verify native image runtime run: ./script/ci/verify-hybrid-collector-native-image.sh apache/hertzbeat-collector:native-test linux/amd64 - name: Build multi-platform native image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: target/native-container-context file: script/docker/collector/Dockerfile.native diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 16d6356b27..a471a9812b 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -85,16 +85,16 @@ jobs: fi ./script/ci/verify-hybrid-collector-jvm-package.sh "$1" linux-arm64 - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Server - uses: docker/build-push-action@v6 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: ./dist file: ./script/docker/server/Dockerfile @@ -103,7 +103,7 @@ jobs: tags: apache/hertzbeat:nightly - name: Build Collector image for verification - uses: docker/build-push-action@v6 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: ./dist file: ./script/docker/collector/Dockerfile diff --git a/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImplTest.java b/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImplTest.java index 6e2b80807b..c416cb4da3 100644 --- a/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImplTest.java +++ b/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImplTest.java @@ -18,16 +18,40 @@ package org.apache.hertzbeat.ai.service.impl; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.usthe.sureness.subject.SubjectSum; +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.hertzbeat.ai.config.McpContextHolder; +import org.apache.hertzbeat.ai.config.SecurityContextToolCallback; +import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext; import org.apache.hertzbeat.ai.sop.registry.SkillRegistry; import org.apache.hertzbeat.base.dao.GeneralConfigDao; import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig; import org.apache.hertzbeat.common.entity.manager.GeneralConfig; +import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent; import org.apache.hertzbeat.common.util.JsonUtil; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.test.util.ReflectionTestUtils; +import reactor.core.publisher.Flux; import org.springframework.context.ApplicationContext; class ChatClientProviderServiceImplTest { @@ -75,6 +99,77 @@ class ChatClientProviderServiceImplTest { assertTrue(service.isConfigured()); } + @Test + void configurationChangeShouldRefreshConfiguredState() { + AtomicReference<GeneralConfig> currentConfig = new AtomicReference<>(); + GeneralConfigDao configDao = proxyConfigDao(currentConfig); + ChatClientProviderServiceImpl service = new ChatClientProviderServiceImpl(null, configDao, null); + + assertFalse(service.isConfigured()); + + currentConfig.set(providerConfig("sk-test")); + service.onAiProviderConfigChange(changeEvent()); + assertTrue(service.isConfigured()); + + currentConfig.set(providerConfig(" ")); + service.onAiProviderConfigChange(changeEvent()); + assertFalse(service.isConfigured()); + } + + @Test + @SuppressWarnings("unchecked") + void streamChatShouldAttachSubjectAndWrapEveryToolCallback() { + ApplicationContext applicationContext = mock(ApplicationContext.class); + SkillRegistry skillRegistry = mock(SkillRegistry.class); + ChatClient chatClient = mock(ChatClient.class); + ChatClient.ChatClientRequestSpec requestSpec = mock(ChatClient.ChatClientRequestSpec.class); + ChatClient.StreamResponseSpec streamSpec = mock(ChatClient.StreamResponseSpec.class); + ToolCallback delegate = mock(ToolCallback.class); + SubjectSum subject = mock(SubjectSum.class); + ChatClientProviderServiceImpl service = new ChatClientProviderServiceImpl( + applicationContext, proxyConfigDao(new AtomicReference<>()), skillRegistry); + + when(applicationContext.getBean("openAiChatClient", ChatClient.class)).thenReturn(chatClient); + when(chatClient.prompt()).thenReturn(requestSpec); + when(requestSpec.messages(anyList())).thenReturn(requestSpec); + when(requestSpec.system(anyString())).thenReturn(requestSpec); + when(requestSpec.tools(any(Object[].class))).thenReturn(requestSpec); + when(requestSpec.toolContext(anyMap())).thenReturn(requestSpec); + when(requestSpec.stream()).thenReturn(streamSpec); + when(streamSpec.content()).thenReturn(Flux.just("answer")); + when(skillRegistry.getAllSkills()).thenReturn(java.util.List.of()); + ReflectionTestUtils.setField(service, "systemResource", new ByteArrayResource( + "skills={dynamically_injected_skills_list}; conversation={current_conversation_id}" + .getBytes(StandardCharsets.UTF_8))); + ReflectionTestUtils.setField(service, "toolCallbackProvider", ToolCallbackProvider.from(delegate)); + + ChatRequestContext context = ChatRequestContext.builder() + .message("question") + .conversationId(42L) + .subject(subject) + .build(); + service.streamChat(context).collectList().block(); + + ArgumentCaptor<Object[]> callbacksCaptor = ArgumentCaptor.forClass(Object[].class); + ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class); + verify(requestSpec).tools(callbacksCaptor.capture()); + verify(requestSpec).toolContext(contextCaptor.capture()); + assertInstanceOf(SecurityContextToolCallback.class, callbacksCaptor.getValue()[0]); + assertSame(subject, McpContextHolder.getSubject(new org.springframework.ai.chat.model.ToolContext( + contextCaptor.getValue()))); + } + + private static GeneralConfigDao proxyConfigDao(AtomicReference<GeneralConfig> currentConfig) { + return (GeneralConfigDao) Proxy.newProxyInstance( + GeneralConfigDao.class.getClassLoader(), + new Class<?>[]{GeneralConfigDao.class}, + (proxy, method, args) -> "findByType".equals(method.getName()) ? currentConfig.get() : null); + } + + private static AiProviderConfigChangeEvent changeEvent() { + return new AiProviderConfigChangeEvent(new StaticApplicationContext()); + } + private static ChatClientProviderServiceImpl newService(GeneralConfigDao generalConfigDao) { return new ChatClientProviderServiceImpl( mock(ApplicationContext.class), diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java index f856752f1e..1a579a2a43 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/NoticeConfigController.java @@ -26,16 +26,19 @@ import jakarta.validation.Valid; import java.util.List; import java.util.Optional; import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.AlerterProperties; import org.apache.hertzbeat.alert.dto.NoticeReceiverMutationResponse; import org.apache.hertzbeat.alert.dto.NoticeReceiverOptionResponse; import org.apache.hertzbeat.alert.dto.NoticeReceiverRequest; import org.apache.hertzbeat.alert.dto.NoticeReceiverResponse; +import org.apache.hertzbeat.alert.notice.NoticeTemplateRenderer; import org.apache.hertzbeat.alert.service.NoticeReceiverContractService; import org.apache.hertzbeat.alert.service.NoticeTemplateMutationException; import org.apache.hertzbeat.common.entity.dto.Message; import org.apache.hertzbeat.common.entity.alerter.NoticeRule; import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate; import org.apache.hertzbeat.alert.service.NoticeConfigService; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.domain.Page; @@ -71,6 +74,9 @@ public class NoticeConfigController { @Autowired private NoticeReceiverContractService noticeReceiverService; + @Autowired + private AlerterProperties alerterProperties; + @PostMapping(path = "/receiver") @Operation(summary = "Add a recipient", description = "Add a recipient") public ResponseEntity<Message<NoticeReceiverMutationResponse>> addNewNoticeReceiver( @@ -317,6 +323,20 @@ public class NoticeConfigController { } } + @PostMapping(path = "/template/preview") + @Operation(summary = "Preview how a notice template renders against a sample alert", + description = "Preview how a notice template renders against a sample alert, without sending anything") + public ResponseEntity<Message<String>> previewNoticeTemplate(@Valid @RequestBody NoticeTemplate noticeTemplate) { + try { + String rendered = NoticeTemplateRenderer.renderContent( + noticeTemplate, NoticeTemplateRenderer.sampleGroupAlert(), alerterProperties.getConsoleUrl(), + ResourceBundleUtil.getBundle("alerter")); + return ResponseEntity.ok(Message.successWithData(rendered)); + } catch (Exception e) { + return ResponseEntity.ok(Message.fail(FAIL_CODE, "Failed to render template: " + e.getMessage())); + } + } + private ResponseEntity<Message<Void>> templateMutationFailure(NoticeTemplateMutationException exception) { String message = switch (exception.getReason()) { case INVALID_REQUEST -> NOTICE_TEMPLATE_INVALID_MESSAGE; diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/NoticeReceiverOptions.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/NoticeReceiverOptions.java index de45533fe0..7d867069df 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/NoticeReceiverOptions.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/NoticeReceiverOptions.java @@ -69,5 +69,13 @@ public class NoticeReceiverOptions { private String serverChanToken; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String gotifyToken; + private String ntfyServerUrl; + private String ntfyTopic; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String ntfyToken; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String wpushToken; + private String wpushChannel; + private String wpushTopicCode; private Set<String> clearSecrets = new LinkedHashSet<>(); } diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/NoticeReceiverRequest.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/NoticeReceiverRequest.java index 564a483e3e..4b4bd85b18 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/NoticeReceiverRequest.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/NoticeReceiverRequest.java @@ -73,6 +73,12 @@ public class NoticeReceiverRequest { case "smnTopicUrn" -> options.setSmnTopicUrn(asString(value)); case "serverChanToken" -> options.setServerChanToken(asString(value)); case "gotifyToken" -> options.setGotifyToken(asString(value)); + case "ntfyServerUrl" -> options.setNtfyServerUrl(asString(value)); + case "ntfyTopic" -> options.setNtfyTopic(asString(value)); + case "ntfyToken" -> options.setNtfyToken(asString(value)); + case "wpushToken" -> options.setWpushToken(asString(value)); + case "wpushChannel" -> options.setWpushChannel(asString(value)); + case "wpushTopicCode" -> options.setWpushTopicCode(asString(value)); case "clearSecrets" -> options.setClearSecrets(asStringSet(value)); case "creator", "modifier", "gmtCreate", "gmtUpdate" -> { } default -> throw new IllegalArgumentException("Unsupported receiver field: " + name); diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java index 4db5112dc3..744be7fb95 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java @@ -336,14 +336,21 @@ public class AlarmGroupReduce implements DisposableBean { long now = System.currentTimeMillis(); String status = determineGroupStatus(snapshot.values()); + boolean hasResolvedAlert = snapshot.values().stream() + .anyMatch(alert -> CommonConstants.ALERT_STATUS_RESOLVED.equals(alert.getStatus())); // For firing alerts, check repeat interval without consuming the retained snapshot. if (CommonConstants.ALERT_STATUS_FIRING.equals(status)) { AlertGroupConverge ruleConfig = groupDefines.get(cache.getGroupDefineName()); - long repeatInterval = ruleConfig.getRepeatInterval() != null + // The rule may have been deleted, renamed or disabled while this group cache + // still holds firing alerts; fall back to the default interval like shouldSendGroup. + long repeatInterval = ruleConfig != null && ruleConfig.getRepeatInterval() != null ? ruleConfig.getRepeatInterval() * MS_PER_SECOND : DEFAULT_REPEAT_INTERVAL; - if (cache.getLastRepeatTime() > 0 + // The throttle only suppresses repeated firing notifications; it must never + // swallow a pending resolved transition. + if (!hasResolvedAlert + && cache.getLastRepeatTime() > 0 && now - cache.getLastRepeatTime() < repeatInterval) { return; } @@ -362,8 +369,16 @@ public class AlarmGroupReduce implements DisposableBean { if (!alarmInhibitReduce.inhibitAlarm(groupAlert)) { return; } - snapshot.forEach((fingerprint, alert) -> - cache.getAlertFingerprints().remove(fingerprint, alert)); + // The resolved members have now been emitted, so drop them from the group. Firing + // members are retained until they recover, keeping the group firing while any member + // is still active instead of flushing the whole cache after every send. + if (hasResolvedAlert) { + snapshot.forEach((fingerprint, alert) -> { + if (CommonConstants.ALERT_STATUS_RESOLVED.equals(alert.getStatus())) { + cache.getAlertFingerprints().remove(fingerprint, alert); + } + }); + } cache.setLastSendTime(now); if (CommonConstants.ALERT_STATUS_FIRING.equals(status)) { cache.setLastRepeatTime(now); diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/NoticeReceiverContractMapper.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/NoticeReceiverContractMapper.java index db57576c97..8360f160b1 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/NoticeReceiverContractMapper.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/NoticeReceiverContractMapper.java @@ -33,7 +33,8 @@ public class NoticeReceiverContractMapper { private static final Set<String> SECRET_FIELDS = Set.of( "hookUrl", "hookAuthToken", "wechatId", "accessToken", "tgBotToken", "slackWebHookUrl", - "appSecret", "discordBotToken", "smnAk", "smnSk", "serverChanToken", "gotifyToken"); + "appSecret", "discordBotToken", "smnAk", "smnSk", "serverChanToken", "gotifyToken", + "ntfyToken", "wpushToken"); private static final Map<Byte, String> TYPE_KEYS = Map.ofEntries( Map.entry((byte) 0, "sms"), Map.entry((byte) 1, "email"), Map.entry((byte) 2, "webhook"), Map.entry((byte) 3, "wechat-official"), Map.entry((byte) 4, "wecom-robot"), @@ -41,7 +42,8 @@ public class NoticeReceiverContractMapper { Map.entry((byte) 7, "telegram-bot"), Map.entry((byte) 8, "slack-webhook"), Map.entry((byte) 9, "discord-bot"), Map.entry((byte) 10, "wecom-app"), Map.entry((byte) 11, "huawei-smn"), Map.entry((byte) 12, "server-chan"), - Map.entry((byte) 13, "gotify"), Map.entry((byte) 14, "feishu-app")); + Map.entry((byte) 13, "gotify"), Map.entry((byte) 14, "feishu-app"), + Map.entry((byte) 15, "ntfy"), Map.entry((byte) 16, "wpush")); private static final Map<Byte, Set<String>> ALLOWED_FIELDS = Map.ofEntries( Map.entry((byte) 0, Set.of("phone")), Map.entry((byte) 1, Set.of("email")), @@ -57,7 +59,9 @@ public class NoticeReceiverContractMapper { Map.entry((byte) 11, Set.of("smnAk", "smnSk", "smnProjectId", "smnRegion", "smnTopicUrn")), Map.entry((byte) 12, Set.of("serverChanToken")), Map.entry((byte) 13, Set.of("gotifyToken")), - Map.entry((byte) 14, Set.of("appId", "appSecret", "larkReceiveType", "userId", "chatId", "partyId"))); + Map.entry((byte) 14, Set.of("appId", "appSecret", "larkReceiveType", "userId", "chatId", "partyId")), + Map.entry((byte) 15, Set.of("ntfyServerUrl", "ntfyTopic", "ntfyToken")), + Map.entry((byte) 16, Set.of("wpushToken", "wpushChannel", "wpushTopicCode"))); public NoticeReceiver toEntity(NoticeReceiverRequest request, NoticeReceiver existing) { Byte type = request.getType(); @@ -106,6 +110,10 @@ public class NoticeReceiverContractMapper { options.setSmnProjectId(receiver.getSmnProjectId()); options.setSmnRegion(receiver.getSmnRegion()); options.setSmnTopicUrn(receiver.getSmnTopicUrn()); + options.setNtfyServerUrl(receiver.getNtfyServerUrl()); + options.setNtfyTopic(receiver.getNtfyTopic()); + options.setWpushChannel(receiver.getWpushChannel()); + options.setWpushTopicCode(receiver.getWpushTopicCode()); options.setClearSecrets(null); Set<String> configuredSecrets = configuredSecrets(receiver); return new NoticeReceiverResponse( @@ -132,6 +140,10 @@ public class NoticeReceiverContractMapper { target.setSmnProjectId(source.getSmnProjectId()); target.setSmnRegion(source.getSmnRegion()); target.setSmnTopicUrn(source.getSmnTopicUrn()); + target.setNtfyServerUrl(source.getNtfyServerUrl()); + target.setNtfyTopic(source.getNtfyTopic()); + target.setWpushChannel(source.getWpushChannel()); + target.setWpushTopicCode(source.getWpushTopicCode()); target.setHookUrl(secret("hookUrl", source.getHookUrl(), existing == null ? null : existing.getHookUrl(), source)); target.setHookAuthToken(secret("hookAuthToken", source.getHookAuthToken(), existing == null ? null : existing.getHookAuthToken(), source)); target.setWechatId(secret("wechatId", source.getWechatId(), existing == null ? null : existing.getWechatId(), source)); @@ -144,6 +156,8 @@ public class NoticeReceiverContractMapper { target.setSmnSk(secret("smnSk", source.getSmnSk(), existing == null ? null : existing.getSmnSk(), source)); target.setServerChanToken(secret("serverChanToken", source.getServerChanToken(), existing == null ? null : existing.getServerChanToken(), source)); target.setGotifyToken(secret("gotifyToken", source.getGotifyToken(), existing == null ? null : existing.getGotifyToken(), source)); + target.setNtfyToken(secret("ntfyToken", source.getNtfyToken(), existing == null ? null : existing.getNtfyToken(), source)); + target.setWpushToken(secret("wpushToken", source.getWpushToken(), existing == null ? null : existing.getWpushToken(), source)); } private String secret(String field, String supplied, String existing, NoticeReceiverOptions options) { @@ -198,6 +212,8 @@ public class NoticeReceiverContractMapper { } case 12 -> require(receiver.getServerChanToken(), "serverChanToken"); case 13 -> require(receiver.getGotifyToken(), "gotifyToken"); + case 15 -> require(receiver.getNtfyTopic(), "ntfyTopic"); + case 16 -> require(receiver.getWpushToken(), "wpushToken"); case 14 -> { require(receiver.getAppId(), "appId"); require(receiver.getAppSecret(), "appSecret"); @@ -245,6 +261,12 @@ public class NoticeReceiverContractMapper { add(fields, "smnTopicUrn", options.getSmnTopicUrn()); add(fields, "serverChanToken", options.getServerChanToken()); add(fields, "gotifyToken", options.getGotifyToken()); + add(fields, "ntfyServerUrl", options.getNtfyServerUrl()); + add(fields, "ntfyTopic", options.getNtfyTopic()); + add(fields, "ntfyToken", options.getNtfyToken()); + add(fields, "wpushToken", options.getWpushToken()); + add(fields, "wpushChannel", options.getWpushChannel()); + add(fields, "wpushTopicCode", options.getWpushTopicCode()); return fields; } @@ -262,6 +284,8 @@ public class NoticeReceiverContractMapper { add(fields, "smnSk", receiver.getSmnSk()); add(fields, "serverChanToken", receiver.getServerChanToken()); add(fields, "gotifyToken", receiver.getGotifyToken()); + add(fields, "ntfyToken", receiver.getNtfyToken()); + add(fields, "wpushToken", receiver.getWpushToken()); return fields; } diff --git a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImpl.java b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImpl.java index 030b65dbc7..2f03bd05cc 100644 --- a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImpl.java +++ b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImpl.java @@ -66,11 +66,8 @@ public class SmsLocalSmsClientImpl implements SmsClient { } try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - String content = alert.getCommonAnnotations().get("summary"); - if (Objects.isNull(content) || Objects.isNull(alert.getCommonAnnotations().get("description"))) { - content = alert.getAlerts().get(0).getContent(); - } - SmsMessage smsMessage = new SmsMessage(FROM, receiver.getPhone(), content); + SmsAlertFields fields = SmsAlertFields.from(alert); + SmsMessage smsMessage = new SmsMessage(FROM, receiver.getPhone(), fields.content()); String payload = JsonUtil.toJson(smsMessage); diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java index c28dc3ac8a..eba72d541c 100644 --- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/controller/NoticeConfigControllerTest.java @@ -81,6 +81,9 @@ class NoticeConfigControllerTest { @Mock private NoticeReceiverContractService noticeReceiverService; + @Mock + private org.apache.hertzbeat.alert.AlerterProperties alerterProperties; + @InjectMocks private NoticeConfigController noticeConfigController; @@ -639,6 +642,44 @@ class NoticeConfigControllerTest { .andReturn(); } + @Test + void previewNoticeTemplate() throws Exception { + NoticeTemplate noticeTemplate = new NoticeTemplate(); + noticeTemplate.setId(5L); + noticeTemplate.setName("preview-test"); + noticeTemplate.setType((byte) 5); + noticeTemplate.setContent(""" + [${title}] status=${status} + <#list alerts as alert> + ${alert.labels.alertname} - ${alert.content} + </#list>"""); + when(alerterProperties.getConsoleUrl()).thenReturn("http://localhost:1157"); + + this.mockMvc.perform(post("/api/notice/template/preview") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(noticeTemplate))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data").value(containsString("HighCPUUsage"))) + .andReturn(); + } + + @Test + void previewNoticeTemplateWithInvalidContent() throws Exception { + NoticeTemplate noticeTemplate = new NoticeTemplate(); + noticeTemplate.setId(5L); + noticeTemplate.setName("preview-test-invalid"); + noticeTemplate.setType((byte) 5); + noticeTemplate.setContent("${undefinedVariable}"); + + this.mockMvc.perform(post("/api/notice/template/preview") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(noticeTemplate))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE)) + .andReturn(); + } + private NoticeReceiverResponse safeResponse(NoticeReceiver receiver) { return new NoticeReceiverContractMapper().toResponse(receiver); } diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduceTest.java index 7b2352b085..45d809b518 100644 --- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduceTest.java +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduceTest.java @@ -17,25 +17,6 @@ * under the License. */ -/* - * 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.alert.reduce; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,6 +25,7 @@ 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.argThat; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.never; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; @@ -51,6 +33,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -64,6 +47,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao; import org.apache.hertzbeat.common.config.VirtualThreadProperties; import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge; +import org.apache.hertzbeat.common.entity.alerter.GroupAlert; import org.apache.hertzbeat.common.entity.alerter.SingleAlert; import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes; import org.junit.jupiter.api.AfterEach; @@ -288,11 +272,14 @@ class AlarmGroupReduceTest { }).when(alarmInhibitReduce).inhibitAlarm(any()); alarmGroupReduce.processGroupAlert(groupAlert("fp-1", "firing")); - dispatchAndDrain(); dispatchAndDrain(); dispatchAndDrain(); - verify(alarmInhibitReduce, times(2)).inhibitAlarm(any()); + ArgumentCaptor<GroupAlert> captor = ArgumentCaptor.forClass(GroupAlert.class); + verify(alarmInhibitReduce, atLeastOnce()).inhibitAlarm(captor.capture()); + assertTrue(captor.getAllValues().stream().anyMatch(group -> + group.getAlerts().stream().anyMatch(a -> "fp-2".equals(a.getFingerprint()))), + "concurrently inserted alert was dropped with the emitted snapshot"); } @Test @@ -313,6 +300,89 @@ class AlarmGroupReduceTest { && group.getWorkspaceId().equals(group.getAlerts().getFirst().getWorkspaceId()))); } + /** + * Regression for issue #4160 (Bug 1): while one member of a group is still firing, the + * recovery of another member must not flip the whole group to resolved. The group has to + * stay firing until every member has actually cleared. + */ + @Test + void whenOneMemberRecoversButAnotherStillFiring_groupMustNotResolve() throws Exception { + alarmGroupReduce.refreshGroupDefines(Collections.singletonList(instanceRule(3600))); + + alarmGroupReduce.processGroupAlert(instanceAlert("cpu", "firing", "host1")); + alarmGroupReduce.processGroupAlert(instanceAlert("mem", "firing", "host1")); + dispatchAndDrain(); + + alarmGroupReduce.processGroupAlert(instanceAlert("cpu", "resolved", "host1")); + dispatchAndDrain(); + + ArgumentCaptor<GroupAlert> captor = ArgumentCaptor.forClass(GroupAlert.class); + verify(alarmInhibitReduce, atLeastOnce()).inhibitAlarm(captor.capture()); + List<GroupAlert> groups = captor.getAllValues(); + + assertTrue(groups.stream().noneMatch(g -> "resolved".equals(g.getStatus())), + "group wrongly resolved while a member alert was still firing"); + assertTrue(groups.stream().anyMatch(g -> "firing".equals(g.getStatus()) + && g.getAlerts().stream().anyMatch( + a -> "cpu".equals(a.getFingerprint()) && "resolved".equals(a.getStatus()))), + "CPU recovery was not communicated within the still-firing group"); + } + + /** + * Regression for issue #4160 (Bug 2): a resolved transition that happens while the group is + * firing and inside the firing repeat-interval window must still be emitted. The firing + * throttle may only suppress repeated firing notifications, never a pending recovery. + */ + @Test + void whenMemberRecoversInsideRepeatInterval_recoveryMustStillBeEmitted() throws Exception { + alarmGroupReduce.refreshGroupDefines(Collections.singletonList(instanceRule(3600))); + + alarmGroupReduce.processGroupAlert(instanceAlert("cpu", "firing", "host1")); + alarmGroupReduce.processGroupAlert(instanceAlert("mem", "firing", "host1")); + dispatchAndDrain(); + + alarmGroupReduce.processGroupAlert(instanceAlert("cpu", "firing", "host1")); + alarmGroupReduce.processGroupAlert(instanceAlert("mem", "resolved", "host1")); + dispatchAndDrain(); + + ArgumentCaptor<GroupAlert> captor = ArgumentCaptor.forClass(GroupAlert.class); + verify(alarmInhibitReduce, atLeastOnce()).inhibitAlarm(captor.capture()); + List<GroupAlert> groups = captor.getAllValues(); + + assertTrue(groups.stream().anyMatch(g -> g.getAlerts().stream().anyMatch( + a -> "mem".equals(a.getFingerprint()) && "resolved".equals(a.getStatus()))), + "memory recovery was silently dropped by the firing repeat-interval throttle"); + } + + /** + * Regression: deleting (or renaming/disabling) a converge rule while its group cache still + * holds a firing alert must not break the periodic group dispatch with an NPE. The orphaned + * group has to fall back to the default repeat interval, like shouldSendGroup already does + * for the group wait/interval. + */ + @Test + void whenRuleDeleted_firingGroupCacheMustStillBeDispatched() throws Exception { + alarmGroupReduce.refreshGroupDefines(Collections.singletonList(instanceRule(3600))); + + alarmGroupReduce.processGroupAlert(instanceAlert("cpu", "firing", "host1")); + + alarmGroupReduce.refreshGroupDefines(Collections.emptyList()); + + Field cachesField = AlarmGroupReduce.class.getDeclaredField("groupCacheMap"); + cachesField.setAccessible(true); + Map<?, ?> caches = (Map<?, ?>) cachesField.get(alarmGroupReduce); + for (Object cache : caches.values()) { + Field createTimeField = cache.getClass().getDeclaredField("createTime"); + createTimeField.setAccessible(true); + createTimeField.setLong(cache, System.currentTimeMillis() - 60_000); + } + + dispatchAndDrain(); + + verify(alarmInhibitReduce, atLeastOnce()).inhibitAlarm(argThat(group -> + group.getAlerts().stream().anyMatch(a -> "cpu".equals(a.getFingerprint())))); + } + private void dispatchAndDrain() throws Exception { alarmGroupReduce.dispatchCheckAndSendGroups(); alarmGroupReduce.pauseAdmission(); @@ -330,6 +400,26 @@ class AlarmGroupReduceTest { return rule; } + private AlertGroupConverge instanceRule(long repeatInterval) { + AlertGroupConverge rule = new AlertGroupConverge(); + rule.setName("instance-rule"); + rule.setGroupLabels(Collections.singletonList("instance")); + rule.setGroupWait(0L); + rule.setGroupInterval(0L); + rule.setRepeatInterval(repeatInterval); + return rule; + } + + private SingleAlert instanceAlert(String fingerprint, String status, String instance) { + return SingleAlert.builder() + .workspaceId(AuthTokenScopes.DEFAULT_WORKSPACE_ID) + .fingerprint(fingerprint) + .status(status) + .labels(createLabels("instance", instance)) + .annotations(new HashMap<>()) + .build(); + } + private SingleAlert groupAlert(String fingerprint, String status) { return groupAlert(AuthTokenScopes.DEFAULT_WORKSPACE_ID, fingerprint, status); } diff --git a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineJsonImExportServiceTest.java b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineJsonImExportServiceTest.java index c11284cdc6..a9301b0a86 100644 --- a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineJsonImExportServiceTest.java +++ b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineJsonImExportServiceTest.java @@ -58,6 +58,7 @@ class AlertDefineJsonImExportServiceTest { alertDefine.setName("App1"); alertDefine.setType("realtime"); alertDefine.setDatasource("lifecycle-promql"); + alertDefine.setQueryLanguage("promql"); alertDefine.setExpr("Expr1"); alertDefine.setPeriod(3000); alertDefine.setTimes(3); @@ -101,9 +102,11 @@ class AlertDefineJsonImExportServiceTest { assertTrue(result.contains("App1")); assertTrue(result.contains("realtime")); assertTrue(result.contains("\"datasource\":\"lifecycle-promql\"")); + assertTrue(result.contains("\"queryLanguage\":\"promql\"")); List<ExportAlertDefineDTO> parsed = service.parseImport( new ByteArrayInputStream(outputStream.toByteArray())); assertEquals("lifecycle-promql", parsed.getFirst().getAlertDefine().getDatasource()); + assertEquals("promql", parsed.getFirst().getAlertDefine().getQueryLanguage()); } @Test diff --git a/hertzbeat-startup/src/main/resources/application-test.yml b/hertzbeat-startup/src/main/resources/application-test.yml index 36f97a3c1c..a4cf3aad1c 100644 --- a/hertzbeat-startup/src/main/resources/application-test.yml +++ b/hertzbeat-startup/src/main/resources/application-test.yml @@ -165,14 +165,6 @@ alerter: access-key-secret: sign-name: template-code: - unisms: - # auth-mode: simple or hmac - auth-mode: simple - access-key-id: YOUR_ACCESS_KEY_ID - # hmac mode need to fill in access-key-secret - access-key-secret: YOUR_ACCESS_KEY_SECRET - signature: YOUR_SMS_SIGNATURE - template-id: YOUR_TEMPLATE_ID smslocal: api-key: YOUR_API_KEY_HERE aws: diff --git a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutorTest.java b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutorTest.java index b380ccf113..f26c520f04 100644 --- a/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutorTest.java +++ b/hertzbeat-warehouse/src/test/java/org/apache/hertzbeat/warehouse/db/GreptimeSqlQueryExecutorTest.java @@ -20,6 +20,7 @@ package org.apache.hertzbeat.warehouse.db; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -29,6 +30,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -88,6 +90,37 @@ class GreptimeSqlQueryExecutorTest { queryGuard.close(); } + @Test + @SuppressWarnings("unchecked") + void testRequestBodyIsFormEncoded() { + when(restTemplate.exchange( + any(String.class), + eq(HttpMethod.POST), + any(HttpEntity.class), + eq(GreptimeSqlQueryContent.class) + )).thenReturn(new ResponseEntity<>(new GreptimeSqlQueryContent(), HttpStatus.OK)); + + // SQL carrying form-urlencoded metacharacters that would pollute the request body + // if concatenated raw: '&' would start a second sql= parameter and override the query. + String sql = "SELECT * FROM t WHERE name = 'x&sql=DROP TABLE t'"; + greptimeSqlQueryExecutor.execute(sql); + + ArgumentCaptor<HttpEntity<String>> captor = ArgumentCaptor.forClass(HttpEntity.class); + verify(restTemplate).exchange( + any(String.class), + eq(HttpMethod.POST), + captor.capture(), + eq(GreptimeSqlQueryContent.class)); + + String body = captor.getValue().getBody(); + assertNotNull(body); + // The body must be a single sql= parameter; the '&' inside the SQL must be encoded. + String encodedValue = body.substring("sql=".length()); + assertFalse(encodedValue.contains("&"), "raw '&' in body causes HTTP parameter pollution"); + // Decoding the value must yield exactly the original SQL, lossless round-trip. + assertEquals(sql, URLDecoder.decode(encodedValue, StandardCharsets.UTF_8)); + } + @Test void testExecuteSuccess() { // Mock successful response diff --git a/script/docker-compose/hertzbeat-mysql-iotdb/docker-compose.yaml b/script/docker-compose/hertzbeat-mysql-iotdb/docker-compose.yaml index 9bef6f8244..910c116576 100644 --- a/script/docker-compose/hertzbeat-mysql-iotdb/docker-compose.yaml +++ b/script/docker-compose/hertzbeat-mysql-iotdb/docker-compose.yaml @@ -31,7 +31,7 @@ services: retries: 5 start_period: 30s ports: - - '13306:3306' + - '127.0.0.1:13306:3306' environment: TZ: Asia/Shanghai MARIADB_ROOT_PASSWORD: 123456 @@ -55,8 +55,8 @@ services: environment: TZ: Asia/Shanghai ports: - - "18181:8181" - - "16667:6667" + - "127.0.0.1:18181:8181" + - "127.0.0.1:16667:6667" volumes: - ./dbdata/iotdbdata:/iotdb/data networks: @@ -68,6 +68,7 @@ services: hostname: hertzbeat restart: always environment: + HERTZBEAT_COLLECTOR_MYSQL_QUERY_ENGINE: auto TZ: Asia/Shanghai LANG: zh_CN.UTF-8 depends_on: @@ -81,7 +82,10 @@ services: - ./ext-lib:/opt/hertzbeat/ext-lib - ./logs:/opt/hertzbeat/logs ports: - - "1157:1157" - - "1158:1158" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1157:1157" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1158:1158" + # OTLP/gRPC ingestion. 14317 rather than the OpenTelemetry standard 4317, which a local + # OTel Collector normally holds - and a clash on a published port stops the container. + - "${HERTZBEAT_OTLP_BIND_ADDRESS:-127.0.0.1}:14317:14317" networks: - hertzbeat diff --git a/script/docker-compose/hertzbeat-mysql-tdengine/docker-compose.yaml b/script/docker-compose/hertzbeat-mysql-tdengine/docker-compose.yaml index 0a30f8b52d..7eeae071be 100644 --- a/script/docker-compose/hertzbeat-mysql-tdengine/docker-compose.yaml +++ b/script/docker-compose/hertzbeat-mysql-tdengine/docker-compose.yaml @@ -31,7 +31,7 @@ services: retries: 5 start_period: 30s ports: - - '13306:3306' + - '127.0.0.1:13306:3306' environment: TZ: Asia/Shanghai MARIADB_ROOT_PASSWORD: 123456 @@ -55,7 +55,7 @@ services: environment: TZ: Asia/Shanghai ports: - - "16041:6041" + - "127.0.0.1:16041:6041" volumes: - ./dbdata/taosdata:/var/lib/taos/ networks: @@ -67,6 +67,7 @@ services: hostname: hertzbeat restart: always environment: + HERTZBEAT_COLLECTOR_MYSQL_QUERY_ENGINE: auto TZ: Asia/Shanghai LANG: zh_CN.UTF-8 depends_on: @@ -80,7 +81,10 @@ services: - ./ext-lib:/opt/hertzbeat/ext-lib - ./logs:/opt/hertzbeat/logs ports: - - "1157:1157" - - "1158:1158" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1157:1157" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1158:1158" + # OTLP/gRPC ingestion. 14317 rather than the OpenTelemetry standard 4317, which a local + # OTel Collector normally holds - and a clash on a published port stops the container. + - "${HERTZBEAT_OTLP_BIND_ADDRESS:-127.0.0.1}:14317:14317" networks: - hertzbeat diff --git a/script/docker-compose/hertzbeat-mysql-victoria-metrics/docker-compose.yaml b/script/docker-compose/hertzbeat-mysql-victoria-metrics/docker-compose.yaml index a18e4986fa..d17e258fca 100644 --- a/script/docker-compose/hertzbeat-mysql-victoria-metrics/docker-compose.yaml +++ b/script/docker-compose/hertzbeat-mysql-victoria-metrics/docker-compose.yaml @@ -31,7 +31,7 @@ services: retries: 5 start_period: 30s ports: - - '13306:3306' + - '127.0.0.1:13306:3306' environment: TZ: Asia/Shanghai MARIADB_ROOT_PASSWORD: 123456 @@ -55,7 +55,7 @@ services: environment: TZ: Asia/Shanghai ports: - - "18428:8428" + - "127.0.0.1:18428:8428" volumes: - ./dbdata/tsdbdata:/victoria-metrics-data networks: @@ -67,6 +67,7 @@ services: hostname: hertzbeat restart: always environment: + HERTZBEAT_COLLECTOR_MYSQL_QUERY_ENGINE: auto TZ: Asia/Shanghai LANG: zh_CN.UTF-8 depends_on: @@ -80,7 +81,10 @@ services: - ./ext-lib:/opt/hertzbeat/ext-lib - ./logs:/opt/hertzbeat/logs ports: - - "1157:1157" - - "1158:1158" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1157:1157" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1158:1158" + # OTLP/gRPC ingestion. 14317 rather than the OpenTelemetry standard 4317, which a local + # OTel Collector normally holds - and a clash on a published port stops the container. + - "${HERTZBEAT_OTLP_BIND_ADDRESS:-127.0.0.1}:14317:14317" networks: - hertzbeat diff --git a/script/docker-compose/hertzbeat-postgresql-greptimedb/docker-compose.yaml b/script/docker-compose/hertzbeat-postgresql-greptimedb/docker-compose.yaml index b5954fdd36..04e5c8fa41 100644 --- a/script/docker-compose/hertzbeat-postgresql-greptimedb/docker-compose.yaml +++ b/script/docker-compose/hertzbeat-postgresql-greptimedb/docker-compose.yaml @@ -32,8 +32,11 @@ services: timeout: 5s retries: 5 start_period: 30s + # Bind data-store ports to localhost only: these services carry default + # credentials (see POSTGRES_PASSWORD / the greptime static user provider) + # and are only meant for the operator on the host, not for remote access. ports: - - '15432:5432' + - '127.0.0.1:15432:5432' environment: POSTGRES_USER: root POSTGRES_PASSWORD: 123456 @@ -46,7 +49,7 @@ services: - hertzbeat greptime: - image: greptime/greptimedb:latest + image: greptime/greptimedb:v1.1.3 container_name: compose-greptimedb hostname: greptime restart: always @@ -58,11 +61,14 @@ services: start_period: 30s environment: TZ: Asia/Shanghai + # Bind data-store ports to localhost only: these services use the default + # static user provider (greptime=greptime) and are only meant for the + # operator on the host, not for remote access. ports: - - "14000:4000" - - "14001:4001" - - "14002:4002" - - "14003:4003" + - "127.0.0.1:14000:4000" + - "127.0.0.1:14001:4001" + - "127.0.0.1:14002:4002" + - "127.0.0.1:14003:4003" volumes: - greptime-tsdb-data:/greptimedb_data command: @@ -100,9 +106,14 @@ services: - ./conf/sureness.yml:/opt/hertzbeat/config/sureness.yml - ./logs:/opt/hertzbeat/logs - ./ext-lib:/opt/hertzbeat/ext-lib + # Keep the quick-start manager and collector listeners local by default. + # Set HERTZBEAT_BIND_ADDRESS explicitly only after replacing the bundled + # credentials and applying the required network access controls. ports: - - "1157:1157" - - "4317:4317" - - "1158:1158" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1157:1157" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1158:1158" + # OTLP/gRPC ingestion. 14317 rather than the OpenTelemetry standard 4317, which a local + # OTel Collector normally holds - and a clash on a published port stops the container. + - "${HERTZBEAT_OTLP_BIND_ADDRESS:-127.0.0.1}:14317:14317" networks: - hertzbeat diff --git a/script/docker-compose/hertzbeat-postgresql-victoria-metrics-observability/docker-compose.yaml b/script/docker-compose/hertzbeat-postgresql-victoria-metrics-observability/docker-compose.yaml index eb1bf28678..3612eb526b 100644 --- a/script/docker-compose/hertzbeat-postgresql-victoria-metrics-observability/docker-compose.yaml +++ b/script/docker-compose/hertzbeat-postgresql-victoria-metrics-observability/docker-compose.yaml @@ -29,8 +29,11 @@ services: timeout: 5s retries: 5 start_period: 30s + # Bind data-store ports to localhost only: these services carry default + # credentials (see POSTGRES_PASSWORD) and are only meant for the operator + # on the host, not for remote access. ports: - - "15432:5432" + - "127.0.0.1:15432:5432" environment: POSTGRES_USER: root POSTGRES_PASSWORD: 123456 @@ -56,7 +59,7 @@ services: environment: TZ: Asia/Shanghai ports: - - "18428:8428" + - "127.0.0.1:18428:8428" volumes: - ./dbdata/tsdbdata:/victoria-metrics-data networks: @@ -81,9 +84,14 @@ services: - ../hertzbeat-postgresql-victoria-metrics/conf/sureness.yml:/opt/hertzbeat/config/sureness.yml - ./logs/hertzbeat:/opt/hertzbeat/logs - ./ext-lib:/opt/hertzbeat/ext-lib + # Keep the quick-start manager and collector listeners local by default. + # Set HERTZBEAT_BIND_ADDRESS explicitly only after replacing the bundled + # credentials and applying the required network access controls. ports: - - "4317:4317" - - "1157:1157" - - "1158:1158" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1157:1157" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1158:1158" + # OTLP/gRPC ingestion. 14317 rather than the OpenTelemetry standard 4317, which a local + # OTel Collector normally holds - and a clash on a published port stops the container. + - "${HERTZBEAT_OTLP_BIND_ADDRESS:-127.0.0.1}:14317:14317" networks: - hertzbeat diff --git a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/docker-compose.yaml b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/docker-compose.yaml index 6bf55aa02d..867d81086d 100644 --- a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/docker-compose.yaml +++ b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/docker-compose.yaml @@ -32,10 +32,10 @@ services: retries: 5 start_period: 30s ports: - - '15432:5432' + - '127.0.0.1:15432:5432' environment: - POSTGRES_USER: root - POSTGRES_PASSWORD: 123456 + POSTGRES_USER: ${POSTGRES_USER:-root} + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?Please set POSTGRES_PASSWORD (e.g., in .env)}" TZ: Asia/Shanghai PGDATA: /var/lib/postgresql/data/pgdata volumes: @@ -58,7 +58,7 @@ services: environment: TZ: Asia/Shanghai ports: - - "18428:8428" + - "127.0.0.1:18428:8428" volumes: - ./dbdata/tsdbdata:/victoria-metrics-data networks: @@ -70,8 +70,11 @@ services: hostname: hertzbeat restart: always environment: + HERTZBEAT_COLLECTOR_MYSQL_QUERY_ENGINE: auto TZ: Asia/Shanghai LANG: zh_CN.UTF-8 + SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-root} + SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:?Please set POSTGRES_PASSWORD in your .env file} depends_on: postgres: condition: service_healthy @@ -83,7 +86,10 @@ services: - ./logs:/opt/hertzbeat/logs - ./ext-lib:/opt/hertzbeat/ext-lib ports: - - "1157:1157" - - "1158:1158" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1157:1157" + - "${HERTZBEAT_BIND_ADDRESS:-127.0.0.1}:1158:1158" + # OTLP/gRPC ingestion. 14317 rather than the OpenTelemetry standard 4317, which a local + # OTel Collector normally holds - and a clash on a published port stops the container. + - "${HERTZBEAT_OTLP_BIND_ADDRESS:-127.0.0.1}:14317:14317" networks: - hertzbeat diff --git a/script/docker/collector/Dockerfile b/script/docker/collector/Dockerfile index d0fa7c3d9c..c3caf203b3 100644 --- a/script/docker/collector/Dockerfile +++ b/script/docker/collector/Dockerfile @@ -24,7 +24,7 @@ ARG TARGETARCH # Install SSH RUN sed -i 's#http://#https://#g' /etc/apt/sources.list.d/ubuntu.sources && \ apt-get update && apt-get install -y openssh-server -RUN mkdir /var/run/sshd +RUN mkdir -p /var/run/sshd ADD apache-hertzbeat-collector-*-bin-linux_${TARGETARCH}.tar.gz /opt/collector-package/ --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
