This is an automated email from the ASF dual-hosted git repository.
gongchao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new 1284b4b46 [feat] Monitoring Center - Added cron expression support for
scheduling type (#3777)
1284b4b46 is described below
commit 1284b4b46b3f8487cb14793912cc9c0aeb43fb12
Author: Albert.Yang <[email protected]>
AuthorDate: Sun Oct 12 00:12:26 2025 +0800
[feat] Monitoring Center - Added cron expression support for scheduling
type (#3777)
Co-authored-by: Calvin <[email protected]>
Co-authored-by: Sherlock Yin <[email protected]>
Co-authored-by: Tomsun28 <[email protected]>
---
.../collector/dispatch/CommonDispatcher.java | 10 +-
.../collector/constants/ScheduleTypeEnum.java | 39 +++++
.../hertzbeat/collector/timer/TimerDispatch.java | 6 +
.../hertzbeat/collector/timer/TimerDispatcher.java | 55 ++++++-
.../collector/timer/TimerDispatcherTest.java | 182 +++++++++++++++++++++
.../apache/hertzbeat/common/entity/job/Job.java | 10 ++
.../hertzbeat/common/entity/manager/Monitor.java | 9 +
.../manager/service/impl/MonitorServiceImpl.java | 4 +
web-app/src/app/pojo/Monitor.ts | 4 +
.../monitor-data-table.component.html | 7 +-
.../monitor-form/monitor-form.component.html | 28 ++++
.../monitor/monitor-form/monitor-form.component.ts | 38 ++++-
web-app/src/assets/i18n/en-US.json | 9 +
web-app/src/assets/i18n/ja-JP.json | 9 +
web-app/src/assets/i18n/pt-BR.json | 9 +
web-app/src/assets/i18n/zh-CN.json | 9 +
web-app/src/assets/i18n/zh-TW.json | 9 +
17 files changed, 419 insertions(+), 18 deletions(-)
diff --git
a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/CommonDispatcher.java
b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/CommonDispatcher.java
index 6e6875624..c03709bad 100644
---
a/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/CommonDispatcher.java
+++
b/hertzbeat-collector/hertzbeat-collector-collector/src/main/java/org/apache/hertzbeat/collector/dispatch/CommonDispatcher.java
@@ -257,10 +257,7 @@ public class CommonDispatcher implements
MetricsTaskDispatch, CollectDataDispatc
// The periodic task pushes the task to the time wheel again.
// First, determine the execution time of the task and the
task collection interval.
if (!timeout.isCancelled()) {
- long spendTime = System.currentTimeMillis() -
job.getDispatchTime();
- long interval = job.getInterval() - spendTime / 1000;
- interval = interval <= 0 ? 0 : interval;
- timerDispatch.cyclicJob(timerJob, interval,
TimeUnit.SECONDS);
+ timerDispatch.cyclicJob(timerJob);
}
} else if (!metricsSet.isEmpty()) {
// The execution of the current level metrics is completed,
and the execution of the next level metrics starts
@@ -360,10 +357,7 @@ public class CommonDispatcher implements
MetricsTaskDispatch, CollectDataDispatc
// The periodic task pushes the task to the time wheel again.
// First, determine the execution time of the task and the task
collection interval.
if (!timeout.isCancelled()) {
- long spendTime = System.currentTimeMillis() -
job.getDispatchTime();
- long interval = job.getInterval() - spendTime / 1000;
- interval = interval <= 0 ? 0 : interval;
- timerDispatch.cyclicJob(timerJob, interval, TimeUnit.SECONDS);
+ timerDispatch.cyclicJob(timerJob);
}
// it is an asynchronous periodic cyclic task, directly response
the collected data
metricsDataList.forEach(commonDataQueue::sendMetricsData);
diff --git
a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/constants/ScheduleTypeEnum.java
b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/constants/ScheduleTypeEnum.java
new file mode 100644
index 000000000..c8ecde161
--- /dev/null
+++
b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/constants/ScheduleTypeEnum.java
@@ -0,0 +1,39 @@
+/*
+ * 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.collector.constants;
+
+import lombok.Getter;
+
+/**
+ * Schedule Type
+ */
+@Getter
+public enum ScheduleTypeEnum {
+
+ INTERVAL("interval"),
+ CRON("cron");
+
+ private final String type;
+
+ ScheduleTypeEnum(String type) {
+ this.type = type;
+ }
+
+
+
+}
diff --git
a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatch.java
b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatch.java
index c7dd1933e..574812d28 100644
---
a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatch.java
+++
b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatch.java
@@ -45,6 +45,12 @@ public interface TimerDispatch {
*/
void cyclicJob(WheelTimerTask timerTask, long interval, TimeUnit timeUnit);
+ /**
+ * Cyclic job
+ * @param timerTask timerTask
+ */
+ void cyclicJob(WheelTimerTask timerTask);
+
/**
* Delete existing job
* @param jobId jobId
diff --git
a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatcher.java
b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatcher.java
index 980f2a914..4aa6e4c02 100644
---
a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatcher.java
+++
b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/timer/TimerDispatcher.java
@@ -17,13 +17,17 @@
package org.apache.hertzbeat.collector.timer;
+import java.time.Duration;
+import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
+
import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.collector.constants.ScheduleTypeEnum;
import
org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
@@ -32,6 +36,7 @@ import org.apache.hertzbeat.common.timer.HashedWheelTimer;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.common.timer.Timer;
import org.springframework.beans.factory.DisposableBean;
+import org.springframework.scheduling.support.CronExpression;
import org.springframework.stereotype.Component;
/**
@@ -58,12 +63,12 @@ public class TimerDispatcher implements TimerDispatch,
DisposableBean {
* jobId - listener
*/
private final Map<Long, CollectResponseEventListener> eventListeners;
-
+
/**
* is dispatcher online running
*/
private final AtomicBoolean started;
-
+
public TimerDispatcher() {
this.wheelTimer = new HashedWheelTimer(r -> {
Thread ret = new Thread(r, "wheelTimer");
@@ -84,7 +89,8 @@ public class TimerDispatcher implements TimerDispatch,
DisposableBean {
}
WheelTimerTask timerJob = new WheelTimerTask(addJob);
if (addJob.isCyclic()) {
- Timeout timeout = wheelTimer.newTimeout(timerJob,
addJob.getInterval(), TimeUnit.SECONDS);
+ Long nextExecutionTime = getNextExecutionInterval(addJob);
+ Timeout timeout = wheelTimer.newTimeout(timerJob,
nextExecutionTime, TimeUnit.SECONDS);
currentCyclicTaskMap.put(addJob.getId(), timeout);
} else {
for (Metrics metric : addJob.getMetrics()) {
@@ -111,6 +117,13 @@ public class TimerDispatcher implements TimerDispatch,
DisposableBean {
}
}
+ @Override
+ public void cyclicJob(WheelTimerTask timerTask) {
+ Job job = timerTask.getJob();
+ Long nextExecutionTime = getNextExecutionInterval(job);
+ cyclicJob(timerTask, nextExecutionTime, TimeUnit.SECONDS);
+ }
+
@Override
public void deleteJob(long jobId, boolean isCyclic) {
if (isCyclic) {
@@ -125,7 +138,7 @@ public class TimerDispatcher implements TimerDispatch,
DisposableBean {
}
}
}
-
+
@Override
public void goOnline() {
currentCyclicTaskMap.forEach((key, value) -> value.cancel());
@@ -134,7 +147,7 @@ public class TimerDispatcher implements TimerDispatch,
DisposableBean {
currentTempTaskMap.clear();
started.set(true);
}
-
+
@Override
public void goOffline() {
started.set(false);
@@ -143,8 +156,8 @@ public class TimerDispatcher implements TimerDispatch,
DisposableBean {
currentTempTaskMap.forEach((key, value) -> value.cancel());
currentTempTaskMap.clear();
}
-
-
+
+
@Override
public void responseSyncJobData(long jobId, List<CollectRep.MetricsData>
metricsDataTemps) {
currentTempTaskMap.remove(jobId);
@@ -153,9 +166,35 @@ public class TimerDispatcher implements TimerDispatch,
DisposableBean {
eventListener.response(metricsDataTemps);
}
}
-
+
@Override
public void destroy() throws Exception {
this.wheelTimer.stop();
}
+
+ public Long getNextExecutionInterval(Job job) {
+ if (ScheduleTypeEnum.CRON.getType().equals(job.getScheduleType()) &&
job.getCronExpression() != null && !job.getCronExpression().isEmpty()) {
+ try {
+ CronExpression cronExpression =
CronExpression.parse(job.getCronExpression());
+ ZonedDateTime nextExecutionTime =
cronExpression.next(ZonedDateTime.now());
+ long delay = Duration.between(ZonedDateTime.now(),
nextExecutionTime).toMillis();
+ // Convert to seconds and ensure non-negative
+ return Math.max(0, delay / 1000);
+ } catch (Exception e) {
+ log.error("Invalid cron expression: {}",
job.getCronExpression(), e);
+ // Fall back to interval scheduling if cron is invalid
+ return job.getInterval();
+ }
+ } else {
+ if (job.getDispatchTime() > 0) {
+ long spendTime = System.currentTimeMillis() -
job.getDispatchTime();
+ // Calculate remaining interval in seconds, preserving
millisecond precision
+ long intervalMs = job.getInterval() * 1000 - spendTime;
+ // Ensure non-negative
+ return Math.max(0, intervalMs / 1000);
+ }
+ return job.getInterval();
+ }
+ }
+
}
diff --git
a/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/TimerDispatcherTest.java
b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/TimerDispatcherTest.java
new file mode 100644
index 000000000..4554ca8f0
--- /dev/null
+++
b/hertzbeat-collector/hertzbeat-collector-common/src/test/java/org/apache/hertzbeat/collector/timer/TimerDispatcherTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.collector.timer;
+
+import org.apache.hertzbeat.collector.constants.ScheduleTypeEnum;
+import org.apache.hertzbeat.common.entity.job.Job;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for TimerDispatcher's getNextExecutionInterval method
+ */
+public class TimerDispatcherTest {
+
+ @InjectMocks
+ private TimerDispatcher timerDispatcher;
+
+ @Mock
+ private Job job;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ /**
+ * Test getNextExecutionInterval with valid CRON expression
+ * Should return the calculated interval based on cron expression
+ */
+ @Test
+ void testGetNextExecutionIntervalWithValidCron() {
+ // Setup - Use a cron expression that runs every minute
+
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType());
+ when(job.getCronExpression()).thenReturn("0 * * * * ?");
+ when(job.getInterval()).thenReturn(60L); // Default interval as
fallback
+
+ // Execute
+ Long result = timerDispatcher.getNextExecutionInterval(job);
+
+ // Verify - Result should be between 0 and 60 seconds
+ // since the cron expression runs every minute
+ assertEquals(true, result >= 0 && result <= 60);
+ }
+
+ /**
+ * Test getNextExecutionInterval with invalid CRON expression
+ * Should fall back to interval scheduling
+ */
+ @Test
+ void testGetNextExecutionIntervalWithInvalidCron() {
+ // Setup - Use an invalid cron expression
+
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType());
+ when(job.getCronExpression()).thenReturn("invalid-cron-expression");
+ when(job.getInterval()).thenReturn(300L); // 5 minutes
+
+ // Execute
+ Long result = timerDispatcher.getNextExecutionInterval(job);
+
+ // Verify - Should fall back to interval value
+ assertEquals(300L, result);
+ }
+
+ /**
+ * Test getNextExecutionInterval with empty CRON expression
+ * Should fall back to interval scheduling
+ */
+ @Test
+ void testGetNextExecutionIntervalWithEmptyCron() {
+ // Setup - Use empty cron expression
+
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType());
+ when(job.getCronExpression()).thenReturn("");
+ when(job.getInterval()).thenReturn(120L); // 2 minutes
+
+ // Execute
+ Long result = timerDispatcher.getNextExecutionInterval(job);
+
+ // Verify - Should fall back to interval value
+ assertEquals(120L, result);
+ }
+
+ /**
+ * Test getNextExecutionInterval with INTERVAL schedule type and no
dispatchTime
+ * Should return the full interval value
+ */
+ @Test
+ void testGetNextExecutionIntervalWithIntervalNoDispatchTime() {
+ // Setup - Use interval schedule type with no dispatchTime
+
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.INTERVAL.getType());
+ when(job.getDispatchTime()).thenReturn(0L); // No dispatchTime set
+ when(job.getInterval()).thenReturn(600L); // 10 minutes
+
+ // Execute
+ Long result = timerDispatcher.getNextExecutionInterval(job);
+
+ // Verify - Should return the full interval value
+ assertEquals(600L, result);
+ }
+
+ /**
+ * Test getNextExecutionInterval with INTERVAL schedule type and
dispatchTime
+ * Should return the remaining interval time
+ */
+ @Test
+ void testGetNextExecutionIntervalWithIntervalAndDispatchTime() {
+ // Setup - Use interval schedule type with dispatchTime
+ long interval = 600L; // 10 minutes in seconds
+ long spendTime = 300000L; // 5 minutes in milliseconds
+ long dispatchTime = System.currentTimeMillis() - spendTime;
+
+
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.INTERVAL.getType());
+ when(job.getDispatchTime()).thenReturn(dispatchTime);
+ when(job.getInterval()).thenReturn(interval);
+
+ // Execute
+ Long result = timerDispatcher.getNextExecutionInterval(job);
+
+ // Verify - Should return the remaining interval time (approximately 5
minutes)
+ // Considering possible time differences during execution, we check a
range
+ assertEquals(true, result >= 295 && result <= 305);
+ }
+
+ /**
+ * Test getNextExecutionInterval with INTERVAL schedule type and negative
remaining time
+ * Should return 0 to ensure non-negative value
+ */
+ @Test
+ void testGetNextExecutionIntervalWithIntervalAndNegativeRemainingTime() {
+ // Setup - Use interval schedule type with dispatchTime resulting in
negative remaining time
+ long interval = 600L; // 10 minutes in seconds
+ long spendTime = 1200000L; // 20 minutes in milliseconds (more than
interval)
+ long dispatchTime = System.currentTimeMillis() - spendTime;
+
+
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.INTERVAL.getType());
+ when(job.getDispatchTime()).thenReturn(dispatchTime);
+ when(job.getInterval()).thenReturn(interval);
+
+ // Execute
+ Long result = timerDispatcher.getNextExecutionInterval(job);
+
+ // Verify - Should return 0 to ensure non-negative value
+ assertEquals(0L, result);
+ }
+
+ /**
+ * Test getNextExecutionInterval with CRON schedule type and null
cronExpression
+ * Should fall back to interval scheduling
+ */
+ @Test
+ void testGetNextExecutionIntervalWithCronAndNullExpression() {
+ // Setup - Use cron schedule type with null cronExpression
+
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType());
+ when(job.getCronExpression()).thenReturn(null);
+ when(job.getInterval()).thenReturn(180L); // 3 minutes
+
+ // Execute
+ Long result = timerDispatcher.getNextExecutionInterval(job);
+
+ // Verify - Should fall back to interval value
+ assertEquals(180L, result);
+ }
+}
\ No newline at end of file
diff --git
a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java
index 6ab4e10b0..4b4e1d532 100644
---
a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java
+++
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/Job.java
@@ -143,6 +143,16 @@ public class Job {
*/
private boolean prometheusProxyMode = false;
+ /**
+ * Scheduling type: interval or cron
+ */
+ private String scheduleType = "interval";
+
+ /**
+ * Cron expression for scheduling, used when scheduleType is "cron"
+ */
+ private String cronExpression = null;
+
/**
* the collect data response metrics as env configmap for other collect
use. ^o^xxx^o^
*/
diff --git
a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java
index c2b5cca16..a549c44c8 100644
---
a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java
+++
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.common.entity.manager;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
+
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
@@ -90,6 +91,14 @@ public class Monitor {
@Min(10)
private Integer intervals;
+ @Schema(title = "Schedule type: interval | cron", example = "interval",
accessMode = READ_WRITE)
+ @Size(max = 20)
+ private String scheduleType;
+
+ @Schema(title = "Cron expression when scheduleType is cron", example =
"0/5 * * * * ?", accessMode = READ_WRITE)
+ @Size(max = 100)
+ private String cronExpression;
+
@Schema(title = "Task status 0: Paused, 1: Up, 2: Down", accessMode =
READ_WRITE)
@Min(0)
@Max(4)
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
index 00496ff3f..b1c689277 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
@@ -515,6 +515,8 @@ public class MonitorServiceImpl implements MonitorService {
appDefine.setDefaultInterval(monitor.getIntervals());
appDefine.setCyclic(true);
appDefine.setTimestamp(System.currentTimeMillis());
+ appDefine.setScheduleType(monitor.getScheduleType());
+ appDefine.setCronExpression(monitor.getCronExpression());
Map<String, String> metadata =
Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
appDefine.setMetadata(metadata);
@@ -762,6 +764,8 @@ public class MonitorServiceImpl implements MonitorService {
appDefine.setDefaultInterval(monitor.getIntervals());
appDefine.setCyclic(true);
appDefine.setTimestamp(System.currentTimeMillis());
+ appDefine.setScheduleType(monitor.getScheduleType());
+ appDefine.setCronExpression(monitor.getCronExpression());
Map<String, String> metadata =
Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
appDefine.setMetadata(metadata);
diff --git a/web-app/src/app/pojo/Monitor.ts b/web-app/src/app/pojo/Monitor.ts
index 5093887d8..619604034 100644
--- a/web-app/src/app/pojo/Monitor.ts
+++ b/web-app/src/app/pojo/Monitor.ts
@@ -24,6 +24,10 @@ export class Monitor {
scrape!: string;
host!: string;
intervals: number = 60;
+ // Schedule type: interval | cron
+ scheduleType: string = 'interval';
+ // Cron expression when scheduleType is cron
+ cronExpression?: string;
// Monitoring status 0: Paused, 1: Up, 2: Down
status!: number;
// Task type 0: Normal, 1: push auto create, 2: discovery auto create
diff --git
a/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.html
b/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.html
index 82a65020f..7aae9843d 100644
---
a/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.html
+++
b/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.html
@@ -53,7 +53,12 @@
<nz-descriptions-item [nzTitle]="'ID'">{{ monitorId
}}</nz-descriptions-item>
<nz-descriptions-item [nzTitle]="'HOST'">{{ monitor.host
}}</nz-descriptions-item>
<nz-descriptions-item [nzTitle]="'monitor.detail.port' |
i18n">{{ port }}</nz-descriptions-item>
- <nz-descriptions-item [nzTitle]="'monitor.intervals' | i18n">{{
monitor.intervals }}s</nz-descriptions-item>
+ <nz-descriptions-item [nzTitle]="'monitor.period' | i18n">
+ <ng-container *ngIf="monitor.scheduleType === 'cron' &&
monitor.cronExpression; else showInterval">
+ {{ monitor.cronExpression }}
+ </ng-container>
+ <ng-template #showInterval> {{ monitor.intervals }}s
</ng-template>
+ </nz-descriptions-item>
<nz-descriptions-item [nzTitle]="'label' | i18n" [nzSpan]="2">
<div class="tags-container">
<ng-container *ngFor="let label of
getObjectEntries(monitor.labels)">
diff --git
a/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.html
b/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.html
index 341a8cb6d..b5bcecd1e 100644
--- a/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.html
+++ b/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.html
@@ -158,6 +158,18 @@
</nz-form-item>
<nz-form-item>
+ <nz-form-label nzSpan="7" [nzFor]="'scheduleType'"
[nzTooltipTitle]="'monitor.scheduleType.tip' | i18n"
+ >{{ 'monitor.scheduleType' | i18n }}
+ </nz-form-label>
+ <nz-form-control nzSpan="8" [nzErrorTip]="'validation.required' |
i18n">
+ <nz-select [(ngModel)]="monitor.scheduleType" name="scheduleType"
nzPlaceHolder="{{ 'monitor.scheduleType.tip' | i18n }}">
+ <nz-option nzValue="interval"
[nzLabel]="'monitor.scheduleType.interval' | i18n"></nz-option>
+ <nz-option nzValue="cron" [nzLabel]="'monitor.scheduleType.cron' |
i18n"></nz-option>
+ </nz-select>
+ </nz-form-control>
+ </nz-form-item>
+
+ <nz-form-item *ngIf="monitor.scheduleType === 'interval'">
<nz-form-label nzSpan="7" [nzFor]="'intervals'"
[nzTooltipTitle]="'monitor.intervals.tip' | i18n"
>{{ 'monitor.intervals' | i18n }}
</nz-form-label>
@@ -174,6 +186,22 @@
</nz-form-control>
</nz-form-item>
+ <nz-form-item *ngIf="monitor.scheduleType === 'cron'">
+ <nz-form-label nzSpan="7" [nzFor]="'cronExpression'"
[nzTooltipTitle]="'monitor.cronExpression.tip' | i18n"
+ >{{ 'monitor.cronExpression' | i18n }}
+ </nz-form-label>
+ <nz-form-control nzSpan="8" [nzErrorTip]="'validation.required' |
i18n">
+ <input
+ nz-input
+ [(ngModel)]="monitor.cronExpression"
+ name="cronExpression"
+ id="cronExpression"
+ placeholder="{{ 'monitor.cronExpression.tip' | i18n }}"
+ required
+ />
+ </nz-form-control>
+ </nz-form-item>
+
<nz-form-item>
<nz-form-label nzSpan="7" [nzFor]="'labels'"
[nzTooltipTitle]="'label.bind.tip' | i18n">{{ 'label.bind' | i18n }}
</nz-form-label>
<nz-form-control nzSpan="8" [nzErrorTip]="'validation.required' |
i18n">
diff --git
a/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.ts
b/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.ts
index 3de0ea45e..19843b24e 100644
--- a/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.ts
+++ b/web-app/src/app/routes/monitor/monitor-form/monitor-form.component.ts
@@ -18,7 +18,7 @@
*/
import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges,
Inject } from '@angular/core';
-import { FormGroup } from '@angular/forms';
+import { FormGroup, FormControl } from '@angular/forms';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { NzNotificationService } from 'ng-zorro-antd/notification';
@@ -64,6 +64,12 @@ export class MonitorFormComponent implements OnChanges {
constructor(private notifySvc: NzNotificationService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService) {}
ngOnChanges(changes: SimpleChanges) {
+ // Initialize scheduleType and cronExpression if not present
+ if (this.monitor && !this.monitor.scheduleType) {
+ this.monitor.scheduleType = 'interval';
+ this.monitor.cronExpression = '';
+ }
+
if (changes.advancedParams && changes.advancedParams.currentValue !==
changes.advancedParams.previousValue) {
for (const advancedParam of changes.advancedParams.currentValue) {
if (advancedParam.display !== false) {
@@ -133,6 +139,14 @@ export class MonitorFormComponent implements OnChanges {
});
return;
}
+
+ // Validate cron expression if scheduleType is cron
+ if (this.monitor.scheduleType === 'cron') {
+ if (!this.monitor.cronExpression ||
!this.isValidCronExpression(this.monitor.cronExpression)) {
+ this.notifySvc.error(this.i18nSvc.fanyi('common.error'),
this.i18nSvc.fanyi('monitor.cronExpression.invalid'));
+ return;
+ }
+ }
this.monitor.host = this.monitor.host?.trim();
this.monitor.name = this.monitor.name?.trim();
// todo Set the host property value separately for now
@@ -257,4 +271,26 @@ export class MonitorFormComponent implements OnChanges {
};
}
}
+
+ /**
+ * Validate if the given string is a valid cron expression
+ *
+ * @param cronExpression The cron expression to validate
+ * @returns True if the cron expression is valid, false otherwise
+ */
+ isValidCronExpression(cronExpression: string): boolean {
+ if (!cronExpression || cronExpression.trim() === '') {
+ return false;
+ }
+
+ // Enhanced cron expression validation supporting common syntax:
+ // - Standard 5-field and 6-field cron expressions
+ // - Step values (0/30, */30)
+ // - Question mark (?) for day of month/week in Quartz-style expressions
+ // - Ranges (1-5), lists (1,3,5), and wildcards (*)
+ const cronRegex =
+
/^\s*(\*|\*\/[0-9]+|[0-9]+(\/[0-9]+)?|[0-9]+-[0-9]+(\/[0-9]+)?|\?)\s+(\*|\*\/[0-9]+|[0-9]+(\/[0-9]+)?|[0-9]+-[0-9]+(\/[0-9]+)?|\?)\s+(\*|\*\/[0-9]+|[0-9]+(\/[0-9]+)?|[0-9]+-[0-9]+(\/[0-9]+)?|\?)\s+(\*|\*\/[0-9]+|[0-9]+(\/[0-9]+)?|[0-9]+-[0-9]+(\/[0-9]+)?|\?)\s+(\*|\*\/[0-9]+|[0-9]+(\/[0-9]+)?|[0-9]+-[0-9]+(\/[0-9]+)?|\?)(\s+(\*|\*\/[0-9]+|[0-9]+(\/[0-9]+)?|[0-9]+-[0-9]+(\/[0-9]+)?|\?))?\s*$/;
+
+ return cronRegex.test(cronExpression);
+ }
}
diff --git a/web-app/src/assets/i18n/en-US.json
b/web-app/src/assets/i18n/en-US.json
index 3e3e46bbf..70cb93e73 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -855,6 +855,15 @@
"monitor.import": "Import Monitor",
"monitor.intervals": "Intervals",
"monitor.intervals.tip": "Monitor the interval time of periodic collection
of data, unit second",
+ "monitor.period": "Monitor Period",
+ "monitor.period.tip": "Monitor execution period expression or time interval",
+ "monitor.scheduleType": "Schedule Type",
+ "monitor.scheduleType.tip": "Select the monitor scheduling type",
+ "monitor.scheduleType.interval": "Second Interval",
+ "monitor.scheduleType.cron": "Cron Expression",
+ "monitor.cronExpression": "Cron Expression",
+ "monitor.cronExpression.tip": "Example: 0/30 * * * * ? means execute every
30 seconds",
+ "monitor.cronExpression.invalid": "Invalid Cron expression format",
"monitor.keyword.tip": "Enter keyword which occurrences need to be
monitored",
"monitor.list": "Monitor List",
"monitor.name": "Task Name",
diff --git a/web-app/src/assets/i18n/ja-JP.json
b/web-app/src/assets/i18n/ja-JP.json
index a826bf578..c0961caa5 100644
--- a/web-app/src/assets/i18n/ja-JP.json
+++ b/web-app/src/assets/i18n/ja-JP.json
@@ -844,6 +844,15 @@
"monitor.import": "モニターをインポート",
"monitor.intervals": "間隔",
"monitor.intervals.tip": "周期的なデータ収集の間隔時間(秒単位)",
+ "monitor.period": "監視期間",
+ "monitor.period.tip": "監視実行期間式または時間間隔",
+ "monitor.scheduleType": "スケジュールタイプ",
+ "monitor.scheduleType.tip": "モニタースケジュールタイプを選択",
+ "monitor.scheduleType.interval": "秒間隔",
+ "monitor.scheduleType.cron": "Cron式",
+ "monitor.cronExpression": "Cron式",
+ "monitor.cronExpression.tip": "例: 0/30 * * * * ? は30秒ごとに実行することを意味します",
+ "monitor.cronExpression.invalid": "Cron式の形式が無効です",
"monitor.keyword.tip": "監視が必要な出現キーワードを入力",
"monitor.list": "モニター一覧",
"monitor.name": "タスク名",
diff --git a/web-app/src/assets/i18n/pt-BR.json
b/web-app/src/assets/i18n/pt-BR.json
index 1a3144630..d1cd0915f 100644
--- a/web-app/src/assets/i18n/pt-BR.json
+++ b/web-app/src/assets/i18n/pt-BR.json
@@ -939,6 +939,15 @@
"monitor.import": "Importar monitor",
"monitor.intervals": "Intervalo de monitoramento",
"monitor.intervals.tip": "Intervalo de tempo para coleta periódica de dados
(em segundos)",
+ "monitor.period": "Período de Monitoramento",
+ "monitor.period.tip": "Expressão de período ou intervalo de tempo da
execução do monitor",
+ "monitor.scheduleType": "Tipo de agendamento",
+ "monitor.scheduleType.tip": "Selecione o tipo de agendamento do monitor",
+ "monitor.scheduleType.interval": "Intervalo em segundos",
+ "monitor.scheduleType.cron": "Expressão Cron",
+ "monitor.cronExpression": "Expressão Cron",
+ "monitor.cronExpression.tip": "Exemplo: 0/30 * * * * ? significa executar a
cada 30 segundos",
+ "monitor.cronExpression.invalid": "Formato da expressão Cron inválido",
"monitor.keyword.tip": "Insira a palavra-chave a ser monitorada",
"monitor.list": "Lista de monitores",
"monitor.name": "Nome da tarefa",
diff --git a/web-app/src/assets/i18n/zh-CN.json
b/web-app/src/assets/i18n/zh-CN.json
index 09988ef53..7f36e84a8 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -858,6 +858,15 @@
"monitor.import": "导入监控",
"monitor.intervals": "监控周期",
"monitor.intervals.tip": "周期性采集数据的时间间隔,单位秒",
+ "monitor.period": "监控周期",
+ "monitor.period.tip": "监控执行的周期表达式或时间间隔",
+ "monitor.scheduleType": "调度类型",
+ "monitor.scheduleType.tip": "选择监控的调度类型",
+ "monitor.scheduleType.interval": "秒间隔",
+ "monitor.scheduleType.cron": "Cron表达式",
+ "monitor.cronExpression": "Cron表达式",
+ "monitor.cronExpression.tip": "如:0/30 * * * * ? 表示每30秒执行一次",
+ "monitor.cronExpression.invalid": "Cron表达式格式不正确",
"monitor.keyword.tip": "输入需要监控的关键字",
"monitor.list": "监控列表",
"monitor.name": "任务名称",
diff --git a/web-app/src/assets/i18n/zh-TW.json
b/web-app/src/assets/i18n/zh-TW.json
index 22aff41aa..9a716351c 100644
--- a/web-app/src/assets/i18n/zh-TW.json
+++ b/web-app/src/assets/i18n/zh-TW.json
@@ -848,6 +848,15 @@
"monitor.import": "導入監控",
"monitor.intervals": "監控周期",
"monitor.intervals.tip": "周期性采集數據的時間間隔,單位秒",
+ "monitor.period": "監控周期",
+ "monitor.period.tip": "監控執行的周期表達式或時間間隔",
+ "monitor.scheduleType": "調度類型",
+ "monitor.scheduleType.tip": "選擇監控的調度類型",
+ "monitor.scheduleType.interval": "秒間隔",
+ "monitor.scheduleType.cron": "Cron表達式",
+ "monitor.cronExpression": "Cron表達式",
+ "monitor.cronExpression.tip": "如:0/30 * * * * ? 表示每30秒執行一次",
+ "monitor.cronExpression.invalid": "Cron表達式格式不正確",
"monitor.keyword.tip": "輸入需要監控的關鍵字",
"monitor.list": "監控列表",
"monitor.name": "任務名稱",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]