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]


Reply via email to