This is an automated email from the ASF dual-hosted git repository.

liutianyou 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 589b0d915 [feature]Support grafana charts for Prometheus (#1658)
589b0d915 is described below

commit 589b0d915d03707ba97882a3f4135cb3aaef1492
Author: Logic <[email protected]>
AuthorDate: Tue Sep 3 23:09:14 2024 +0800

    [feature]Support grafana charts for Prometheus (#1658)
    
    Signed-off-by: Logic <[email protected]>
    Signed-off-by: tomsun28 <[email protected]>
    Co-authored-by: tomsun28 <[email protected]>
    Co-authored-by: liutianyou <[email protected]>
    Co-authored-by: aias00 <[email protected]>
---
 .../common/entity/grafana/GrafanaDashboard.java    |  65 +++++
 .../common/entity/grafana/ServiceAccount.java      |  61 ++++
 .../common/entity/grafana/ServiceToken.java        |  50 ++++
 .../hertzbeat/common/entity/manager/Monitor.java   |   9 +
 grafana/pom.xml                                    |  56 ++++
 .../hertzbeat/grafana/common/CommonConstants.java  |  77 +++++
 .../grafana/config/GrafanaAutoConfiguration.java   |  29 ++
 .../hertzbeat/grafana/config/GrafanaInit.java      |  65 +++++
 .../grafana/config/GrafanaProperties.java          |  58 ++++
 .../grafana/controller/DashboardController.java    | 111 ++++++++
 .../controller/ServiceAccountController.java       | 119 ++++++++
 .../apache/hertzbeat/grafana/dao/DashboardDao.java |  48 ++++
 .../hertzbeat/grafana/dao/ServiceAccountDao.java   |  38 +++
 .../hertzbeat/grafana/dao/ServiceTokenDao.java     |  37 +++
 .../grafana/service/DashboardService.java          | 181 ++++++++++++
 .../grafana/service/DatasourceService.java         | 134 +++++++++
 .../grafana/service/ServiceAccountService.java     | 309 +++++++++++++++++++++
 .../src/main/resources/META-INF/spring.factories   |  17 ++
 manager/pom.xml                                    |   5 +
 .../manager/service/impl/MonitorServiceImpl.java   |  23 +-
 manager/src/main/resources/application-test.yml    |   6 +
 manager/src/main/resources/application.yml         |   7 +-
 manager/src/main/resources/sureness.yml            |   4 +
 pom.xml                                            |   7 +
 web-app/src/app/pojo/GrafanaDashboard.ts           |  26 ++
 web-app/src/app/pojo/Monitor.ts                    |   2 +
 web-app/src/app/routes/SafePipe.ts                 |  31 +++
 .../monitor-detail/monitor-detail.component.html   |   9 +
 .../monitor-detail/monitor-detail.component.ts     |  16 ++
 .../monitor/monitor-edit/monitor-edit.component.ts |  17 ++
 .../monitor-form/monitor-form.component.html       |  33 +++
 .../monitor/monitor-form/monitor-form.component.ts |  14 +
 .../monitor/monitor-list/monitor-list.component.ts |  19 ++
 .../monitor/monitor-new/monitor-new.component.ts   |   2 +
 web-app/src/app/routes/monitor/monitor.module.ts   |   2 +
 web-app/src/app/service/monitor.service.ts         |   9 +
 web-app/src/assets/i18n/en-US.json                 |   8 +-
 web-app/src/assets/i18n/zh-CN.json                 |   6 +
 web-app/src/assets/i18n/zh-TW.json                 |   8 +-
 39 files changed, 1714 insertions(+), 4 deletions(-)

diff --git 
a/common/src/main/java/org/apache/hertzbeat/common/entity/grafana/GrafanaDashboard.java
 
b/common/src/main/java/org/apache/hertzbeat/common/entity/grafana/GrafanaDashboard.java
new file mode 100644
index 000000000..f7ab207ff
--- /dev/null
+++ 
b/common/src/main/java/org/apache/hertzbeat/common/entity/grafana/GrafanaDashboard.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.entity.grafana;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.Transient;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+
+
+
+/**
+ * Grafana dashboard entity
+ */
+@Entity
+@Table(name = "hzb_grafana_dashboard")
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(description = "Grafana dashboard entity")
+public class GrafanaDashboard implements Serializable {
+    @Schema(description = "Dashboard folderUid")
+    private String folderUid;
+    @Schema(description = "Dashboard slug")
+    private String slug;
+    @Schema(description = "Dashboard status")
+    private String status;
+    @Schema(description = "Dashboard uid")
+    private String uid;
+    @Schema(description = "Dashboard url")
+    private String url;
+    @Schema(description = "Dashboard version")
+    private Long version;
+    @Id
+    @Schema(description = "Monitor id")
+    private Long monitorId;
+    @Schema(description = "is enabled")
+    private boolean enabled;
+    @Schema(description = "template")
+    @Transient
+    private String template;
+}
diff --git 
a/common/src/main/java/org/apache/hertzbeat/common/entity/grafana/ServiceAccount.java
 
b/common/src/main/java/org/apache/hertzbeat/common/entity/grafana/ServiceAccount.java
new file mode 100644
index 000000000..6aee87e07
--- /dev/null
+++ 
b/common/src/main/java/org/apache/hertzbeat/common/entity/grafana/ServiceAccount.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.entity.grafana;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+
+
+
+/**
+ * Grafana service account entity
+ */
+@Entity
+@Table(name = "hzb_grafana_service_account")
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(description = "Grafana service account entity")
+public class ServiceAccount {
+    @Id
+    @Schema(description = "Service account id")
+    private Long id;
+    @Schema(description = "Service account name")
+    private String name;
+    @Schema(description = "Service account role")
+    private String role;
+    @Schema(description = "Service account is disabled")
+    private Boolean isDisabled;
+    @Schema(description = "Service account tokens")
+    private Integer tokens;
+    @Schema(description = "Service account avatar url")
+    private String avatarUrl;
+    @Schema(description = "Service account login")
+    private String login;
+    @Schema(description = "Service account orgId")
+    private Integer orgId;
+
+}
diff --git 
a/common/src/main/java/org/apache/hertzbeat/common/entity/grafana/ServiceToken.java
 
b/common/src/main/java/org/apache/hertzbeat/common/entity/grafana/ServiceToken.java
new file mode 100644
index 000000000..1ffcc9e81
--- /dev/null
+++ 
b/common/src/main/java/org/apache/hertzbeat/common/entity/grafana/ServiceToken.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.entity.grafana;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+
+/**
+ * Grafana service token entity
+ */
+@Entity
+@Table(name = "hzb_grafana_service_token")
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(description = "Grafana service token entity")
+public class ServiceToken {
+    @Id
+    @Schema(description = "Service token id")
+    private Long id;
+    @Schema(description = "Service token name")
+    private String name;
+    @Schema(description = "Service token key")
+    @Column(name = "`key`")
+    private String key;
+}
diff --git 
a/common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java
index 2d3665bf1..c76051d3f 100644
--- 
a/common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java
+++ 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java
@@ -32,6 +32,7 @@ import jakarta.persistence.JoinColumn;
 import jakarta.persistence.JoinTable;
 import jakarta.persistence.ManyToMany;
 import jakarta.persistence.Table;
+import jakarta.persistence.Transient;
 import jakarta.validation.constraints.Max;
 import jakarta.validation.constraints.Min;
 import jakarta.validation.constraints.Size;
@@ -41,6 +42,7 @@ import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard;
 import org.apache.hertzbeat.common.support.valid.HostValid;
 import org.springframework.data.annotation.CreatedBy;
 import org.springframework.data.annotation.CreatedDate;
@@ -166,4 +168,11 @@ public class Monitor {
         joinColumns = {@JoinColumn(name = "monitor_id", referencedColumnName = 
"id")},
         inverseJoinColumns = {@JoinColumn(name = "tag_id", 
referencedColumnName = "id")})
     private List<Tag> tags;
+
+    /**
+     * grafana dashboard
+     */
+    @Schema(title = "grafana dashboard")
+    @Transient
+    private GrafanaDashboard grafanaDashboard;
 }
diff --git a/grafana/pom.xml b/grafana/pom.xml
new file mode 100644
index 000000000..8e5afd8b4
--- /dev/null
+++ b/grafana/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.hertzbeat</groupId>
+        <artifactId>hertzbeat</artifactId>
+        <version>2.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>hertzbeat-grafana</artifactId>
+    <name>${project.artifactId}</name>
+    <properties>
+        <maven-jar-plugin.version>3.2.0</maven-jar-plugin.version>
+        <maven-assembly-plugin.version>3.3.0</maven-assembly-plugin.version>
+    </properties>
+
+    <dependencies>
+        <!-- hertzbeat common -->
+        <dependency>
+            <groupId>org.apache.hertzbeat</groupId>
+            <artifactId>hertzbeat-common</artifactId>
+        </dependency>
+        <!-- hertzbeat warehouse -->
+        <dependency>
+            <groupId>org.apache.hertzbeat</groupId>
+            <artifactId>hertzbeat-warehouse</artifactId>
+        </dependency>
+        <!-- jpa -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+        <!-- swagger -->
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+        </dependency>
+    </dependencies>
+</project>
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/common/CommonConstants.java
 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/common/CommonConstants.java
new file mode 100644
index 000000000..7c227d0e5
--- /dev/null
+++ 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/common/CommonConstants.java
@@ -0,0 +1,77 @@
+/*
+ * 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.grafana.common;
+
+/**
+ * Grafana Common Constants
+ */
+public interface CommonConstants {
+    String HTTP = "http://";;
+
+    String HTTPS = "https://";;
+    
+    String KIOSK = "?kiosk=tv";
+
+    String REFRESH = "&refresh=15s";
+
+    String INSTANCE = "&var-instance=";
+
+    String AUTHORIZATION = "Authorization";
+    
+    String BEARER = "Bearer ";
+    
+    String CREATE_DASHBOARD_API = "/api/dashboards/db";
+    
+    String DELETE_DASHBOARD_API = "/api/dashboards/uid/%s";
+    
+    String DATASOURCE_NAME = "hertzbeat-victoria-metrics";
+    
+    String DATASOURCE_TYPE = "prometheus";
+    
+    String DATASOURCE_ACCESS = "proxy";
+    
+    String CREATE_DATASOURCE_API = "%s:%s@%s/api/datasources";
+    
+    String DELETE_DATASOURCE_API = "%s:%s@%s/api/datasources/name/%s";  
+    
+    String ACCOUNT_NAME = "hertzbeat";
+    
+    String ACCOUNT_ROLE = "Admin";
+    
+    String ACCOUNT_TOKEN_NAME = "hertzbeat-token";
+    
+    String CREATE_SERVICE_ACCOUNT_API = "%s:%s@%s/api/serviceaccounts";
+    
+    String GET_SERVICE_ACCOUNTS_API = "%s:%s@%s/api/serviceaccounts/search";
+    
+    String DELETE_SERVICE_ACCOUNT_API = "%s:%s@%s/api/serviceaccounts/%d";
+    
+    String CREATE_SERVICE_TOKEN_API = "%s:%s@%s/api/serviceaccounts/%d/tokens";
+    
+    String GET_SERVICE_TOKENS_API = "%s:%s@%s/api/serviceaccounts/%d/tokens";
+
+    String NAME = "name";
+
+    String APPLICATION_JSON = "application/json";
+    
+    String HERTZBEAT_TOKEN = "hertzbeat-token";
+
+    String TYPE = "type";
+
+    String URL = "url";
+}
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaAutoConfiguration.java
 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaAutoConfiguration.java
new file mode 100644
index 000000000..8cf6fd094
--- /dev/null
+++ 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaAutoConfiguration.java
@@ -0,0 +1,29 @@
+/*
+ * 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.grafana.config;
+
+import 
org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.ComponentScan;
+
+/**
+ * Grafana auto configuration.
+ */
+@ComponentScan(basePackages = "org.apache.hertzbeat.grafana")
+@EnableConfigurationProperties(GrafanaProperties.class)
+public class GrafanaAutoConfiguration {
+}
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaInit.java 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaInit.java
new file mode 100644
index 000000000..6306dbeb9
--- /dev/null
+++ b/grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaInit.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.grafana.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.grafana.service.DatasourceService;
+import org.apache.hertzbeat.grafana.service.ServiceAccountService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+/**
+ * grafana init
+ */
+@Component
+@Slf4j
+public class GrafanaInit implements CommandLineRunner {
+    @Autowired
+    private GrafanaProperties grafanaProperties;
+    @Autowired
+    private ServiceAccountService serviceAccountService;
+    @Autowired
+    private DatasourceService datasourceService;
+
+    //1. Determine whether the configuration is filled out completely
+    //2. Determine whether there is an account, if not, create and ensure that 
the account is unique
+    //3. Determine whether there is a token, if not, create and ensure that 
the account is unique.
+    @Override
+    public void run(String... args) throws Exception {
+        if (grafanaProperties.enabled() && grafanaProperties.url() != null && 
grafanaProperties.username() != null && grafanaProperties.password() != null) {
+            serviceAccountService.reload();
+            try {
+                serviceAccountService.getAccount();
+            } catch (RuntimeException e) {
+                log.error("service account is not exist, create service 
account");
+                serviceAccountService.createServiceAccount();
+            }
+            try {
+                serviceAccountService.getToken();
+            } catch (RuntimeException e) {
+                log.error("service token is not exist, create service token");
+                serviceAccountService.createToken();
+            }
+            datasourceService.deleteDatasource();
+            datasourceService.createDatasource();
+        }
+    }
+
+
+}
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaProperties.java
 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaProperties.java
new file mode 100644
index 000000000..77544bba7
--- /dev/null
+++ 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/config/GrafanaProperties.java
@@ -0,0 +1,58 @@
+/*
+ * 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.grafana.config;
+
+import static org.apache.hertzbeat.grafana.common.CommonConstants.HTTP;
+import static org.apache.hertzbeat.grafana.common.CommonConstants.HTTPS;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.bind.DefaultValue;
+
+/**
+ * grafana configuration
+ */
+@Slf4j
+@ConfigurationProperties(prefix = "grafana")
+public record GrafanaProperties(@DefaultValue("false") boolean enabled,
+                                @DefaultValue("http://127.0.0.1:3000";) String 
url,
+                                @DefaultValue("admin") String username,
+                                @DefaultValue("admin") String password) {
+    /**
+     * get the prefix of the grafana url, such as http or https
+     */
+    public String getPrefix() {
+        if (url.startsWith(HTTP)) {
+            return HTTP;
+        } else if (url.startsWith(HTTPS)) {
+            return HTTPS;
+        }
+        return HTTP;
+    }
+
+    /**
+     * get the grafana url without the prefix, such as localhost:3000
+     */
+    public String getUrl() {
+        if (getPrefix().equals(HTTP)) {
+            return url.replace(HTTP, "");
+        } else if (getPrefix().equals(HTTPS)) {
+            return url.replace(HTTPS, "");
+        }
+        return url;
+    }
+}
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/controller/DashboardController.java
 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/controller/DashboardController.java
new file mode 100644
index 000000000..754eb0361
--- /dev/null
+++ 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/controller/DashboardController.java
@@ -0,0 +1,111 @@
+/*
+ * 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.grafana.controller;
+
+import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.entity.dto.Message;
+import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard;
+import org.apache.hertzbeat.grafana.config.GrafanaProperties;
+import org.apache.hertzbeat.grafana.service.DashboardService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Controller for managing Grafana dashboards via API.
+ */
+@Slf4j
+@Tag(name = "Dashboard API")
+@RestController
+@RequestMapping(path = "/api/grafana/dashboard", produces = 
{APPLICATION_JSON_VALUE})
+public class DashboardController {
+
+    @Autowired
+    private DashboardService dashboardService;
+
+    @Autowired
+    private GrafanaProperties grafanaProperties;
+
+    /**
+     * Creates a new Grafana dashboard.
+     *
+     * @param dashboardJson JSON representation of the dashboard
+     * @param monitorId the ID of the monitor associated with the dashboard
+     * @return ResponseEntity containing success or failure message
+     */
+    @Operation(summary = "Create dashboard", description = "Create dashboard")
+    @PostMapping
+    public ResponseEntity<Message<?>> createDashboard(@RequestParam String 
dashboardJson, @RequestParam Long monitorId) {
+        try {
+            ResponseEntity<?> response = 
dashboardService.createDashboard(dashboardJson, monitorId);
+            if (response.getStatusCode().is2xxSuccessful()) {
+                return ResponseEntity.ok(Message.success("create dashboard 
success"));
+            }
+        } catch (Exception e) {
+            log.error("create dashboard error", e);
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, e.getMessage()));
+        }
+        return ResponseEntity.ok(Message.fail(FAIL_CODE, "create dashboard 
fail"));
+    }
+
+    /**
+     * Retrieves a Grafana dashboard by monitor ID.
+     *
+     * @param monitorId the ID of the monitor associated with the dashboard
+     * @return ResponseEntity containing the GrafanaDashboard object or 
failure message
+     */
+    @Operation(summary = "Get dashboard by monitor id", description = "Get 
dashboard by monitor id")
+    @GetMapping
+    public ResponseEntity<Message<?>> getDashboardByMonitorId(@RequestParam 
Long monitorId) {
+        try {
+            GrafanaDashboard grafanaDashboard = 
dashboardService.getDashboardByMonitorId(monitorId);
+            return ResponseEntity.ok(Message.success(grafanaDashboard));
+        } catch (Exception e) {
+            log.error("get dashboard error", e);
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "get dashboard 
fail"));
+        }
+    }
+
+    /**
+     * Deletes a Grafana dashboard by monitor ID.
+     *
+     * @param monitorId the ID of the monitor associated with the dashboard
+     * @return ResponseEntity containing success or failure message
+     */
+    @Operation(summary = "Delete dashboard by monitor id", description = 
"Delete dashboard by monitor id")
+    @DeleteMapping
+    public ResponseEntity<Message<String>> 
deleteDashboardByMonitorId(@RequestParam Long monitorId) {
+        try {
+            dashboardService.deleteDashboard(monitorId);
+        } catch (Exception e) {
+            log.error("delete dashboard error", e);
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "delete dashboard 
fail"));
+        }
+        return ResponseEntity.ok(Message.fail(FAIL_CODE, "delete dashboard 
fail"));
+    }
+}
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/controller/ServiceAccountController.java
 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/controller/ServiceAccountController.java
new file mode 100644
index 000000000..983fab1e3
--- /dev/null
+++ 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/controller/ServiceAccountController.java
@@ -0,0 +1,119 @@
+/*
+ * 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.grafana.controller;
+
+import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.hertzbeat.common.entity.dto.Message;
+import org.apache.hertzbeat.grafana.service.ServiceAccountService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Controller for managing Grafana service accounts and tokens via API.
+ */
+@Tag(name = "Service Account API")
+@RestController
+@RequestMapping(path = "/api/grafana/service-account", produces = 
{APPLICATION_JSON_VALUE})
+public class ServiceAccountController {
+
+    @Autowired
+    private ServiceAccountService serviceAccountService;
+
+    /**
+     * Creates a new service admin account.
+     *
+     * @return ResponseEntity containing the result of the account creation
+     */
+    @PostMapping(path = "/account")
+    @Operation(summary = "Create service account", description = "Create 
service account")
+    public ResponseEntity<Message<?>> createServiceAccount() {
+        try {
+            ResponseEntity<String> response = 
serviceAccountService.createServiceAccount();
+            return handleResponse(response);
+        } catch (Exception e) {
+            return handleException(e);
+        }
+    }
+
+    /**
+     * Retrieves all service accounts.
+     *
+     * @return ResponseEntity containing the list of service accounts
+     */
+    @GetMapping(path = "/accounts")
+    @Operation(summary = "Get service account", description = "Get service 
account")
+    public ResponseEntity<Message<?>> getServiceAccount() {
+        try {
+            ResponseEntity<String> response = 
serviceAccountService.getAccounts();
+            return handleResponse(response);
+        } catch (Exception e) {
+            return handleException(e);
+        }
+    }
+
+    /**
+     * Creates a new API token for a service account.
+     *
+     * @return ResponseEntity containing the result of the token creation
+     */
+    @PostMapping(path = "/token")
+    @Operation(summary = "Create service account token", description = "Create 
service account token")
+    public ResponseEntity<Message<?>> createToken() {
+        try {
+            ResponseEntity<String> response = 
serviceAccountService.createToken();
+            return handleResponse(response);
+        } catch (Exception e) {
+            return handleException(e);
+        }
+    }
+
+    /**
+     * Retrieves all API tokens for service accounts.
+     *
+     * @return ResponseEntity containing the list of tokens
+     */
+    @GetMapping(path = "/tokens")
+    @Operation(summary = "Get service account token", description = "Get 
service account token")
+    public ResponseEntity<Message<?>> getToken() {
+        try {
+            ResponseEntity<String> response = 
serviceAccountService.getTokens();
+            return handleResponse(response);
+        } catch (Exception e) {
+            return handleException(e);
+        }
+    }
+
+    private ResponseEntity<Message<?>> handleResponse(ResponseEntity<String> 
response) {
+        if (response.getStatusCode().is2xxSuccessful()) {
+            return ResponseEntity.ok(Message.success(response.getBody()));
+        }
+        return ResponseEntity.ok(Message.fail(FAIL_CODE, response.getBody()));
+    }
+
+    private ResponseEntity<Message<?>> handleException(Exception e) {
+        return ResponseEntity.ok(Message.fail(FAIL_CODE, e.getMessage()));
+    }
+}
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/dao/DashboardDao.java 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/dao/DashboardDao.java
new file mode 100644
index 000000000..126cab796
--- /dev/null
+++ b/grafana/src/main/java/org/apache/hertzbeat/grafana/dao/DashboardDao.java
@@ -0,0 +1,48 @@
+/*
+ * 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.grafana.dao;
+
+import java.util.List;
+import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+/**
+ * Dashboard Dao
+ */
+public interface DashboardDao extends JpaRepository<GrafanaDashboard, Long>, 
JpaSpecificationExecutor<GrafanaDashboard> {
+    /**
+     * find by monitor id
+     * @param monitorId monitor id
+     * @return dashboard
+     */
+    GrafanaDashboard findByMonitorId(Long monitorId);
+
+    /**
+     * delete by monitor id
+     * @param monitorId monitor id
+     */
+    void deleteByMonitorId(Long monitorId);
+
+    /**
+     * find by dashboard uid
+     * @param dashboardUid dashboard uid
+     * @return dashboard
+     */
+    List<GrafanaDashboard> findByUid(String dashboardUid);
+}
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/dao/ServiceAccountDao.java 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/dao/ServiceAccountDao.java
new file mode 100644
index 000000000..ec47f495f
--- /dev/null
+++ 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/dao/ServiceAccountDao.java
@@ -0,0 +1,38 @@
+/*
+ * 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.grafana.dao;
+
+import org.apache.hertzbeat.common.entity.grafana.ServiceAccount;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * ServiceAccount Dao
+ */
+public interface ServiceAccountDao extends JpaRepository<ServiceAccount, 
Long>, JpaSpecificationExecutor<ServiceAccount> {
+
+    ServiceAccount findByName(String name);
+
+    @Transactional
+    @Modifying
+    @Query(value = "truncate table hzb_grafana_service_account", nativeQuery = 
true)
+    void truncate();
+}
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/dao/ServiceTokenDao.java 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/dao/ServiceTokenDao.java
new file mode 100644
index 000000000..bb7d67e56
--- /dev/null
+++ 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/dao/ServiceTokenDao.java
@@ -0,0 +1,37 @@
+/*
+ * 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.grafana.dao;
+
+import org.apache.hertzbeat.common.entity.grafana.ServiceToken;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * ServiceAccount Dao
+ */
+public interface ServiceTokenDao extends JpaRepository<ServiceToken, Long>, 
JpaSpecificationExecutor<ServiceToken> {
+    ServiceToken findByName(String name);
+
+    @Transactional
+    @Modifying
+    @Query(value = "truncate table hzb_grafana_service_token", nativeQuery = 
true)
+    void truncate();
+}
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/service/DashboardService.java
 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/service/DashboardService.java
new file mode 100644
index 000000000..2249743f2
--- /dev/null
+++ 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/service/DashboardService.java
@@ -0,0 +1,181 @@
+/*
+ * 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.grafana.service;
+
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.CREATE_DASHBOARD_API;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.DELETE_DASHBOARD_API;
+import static org.apache.hertzbeat.grafana.common.CommonConstants.INSTANCE;
+import static org.apache.hertzbeat.grafana.common.CommonConstants.KIOSK;
+import static org.apache.hertzbeat.grafana.common.CommonConstants.REFRESH;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard;
+import org.apache.hertzbeat.common.util.JsonUtil;
+import org.apache.hertzbeat.grafana.config.GrafanaProperties;
+import org.apache.hertzbeat.grafana.dao.DashboardDao;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestTemplate;
+import lombok.extern.slf4j.Slf4j;
+
+
+/**
+ * Service for managing Grafana dashboards.
+ */
+@Service
+@Slf4j
+public class DashboardService {
+
+    @Autowired
+    private ServiceAccountService serviceAccountService;
+
+    @Autowired
+    private DashboardDao dashboardDao;
+
+    @Autowired
+    private GrafanaProperties grafanaProperties;
+
+    @Autowired
+    private RestTemplate restTemplate;
+
+    /**
+     * Creates a new dashboard in Grafana.
+     *
+     * @param dashboardJson the JSON representation of the dashboard
+     * @param monitorId the ID of the monitor associated with the dashboard
+     * @return ResponseEntity containing the response from Grafana
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public ResponseEntity<?> createDashboard(String dashboardJson, Long 
monitorId) {
+        String token = serviceAccountService.getToken();
+        String url = grafanaProperties.getPrefix() + 
grafanaProperties.getUrl() + CREATE_DASHBOARD_API;
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.setBearerAuth(token);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("dashboard", JsonUtil.fromJson(dashboardJson, Object.class));
+        body.put("overwrite", true);
+
+        HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(body, 
headers);
+
+        try {
+            ResponseEntity<String> response = restTemplate.postForEntity(url, 
requestEntity, String.class);
+
+            if (response.getStatusCode().is2xxSuccessful()) {
+                GrafanaDashboard grafanaDashboard = 
JsonUtil.fromJson(response.getBody(), GrafanaDashboard.class);
+                if (grafanaDashboard != null) {
+                    grafanaDashboard.setEnabled(true);
+                    grafanaDashboard.setUrl(grafanaProperties.getPrefix() + 
grafanaProperties.getUrl()
+                            + 
grafanaDashboard.getUrl().replace(grafanaProperties.getUrl(), "") + KIOSK + 
REFRESH + INSTANCE + monitorId);
+                    grafanaDashboard.setMonitorId(monitorId);
+                    dashboardDao.save(grafanaDashboard);
+                    log.info("create dashboard success, token: {}", 
response.getBody());
+                }
+                return response;
+            } else {
+                log.error("create dashboard error: {}", 
response.getStatusCode());
+                throw new RuntimeException("create dashboard error");
+            }
+        } catch (Exception ex) {
+            log.error("create dashboard error", ex);
+            throw new RuntimeException("create dashboard error", ex);
+        }
+    }
+
+
+    /**
+     * Deletes a dashboard in Grafana by monitor ID.
+     *
+     * @param monitorId the ID of the monitor associated with the dashboard
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteDashboard(Long monitorId) {
+        GrafanaDashboard grafanaDashboard = 
dashboardDao.findByMonitorId(monitorId);
+        String uid = grafanaDashboard.getUid();
+        List<GrafanaDashboard> grafanaDashboards = dashboardDao.findByUid(uid);
+
+        if (grafanaDashboards.size() > 1) {
+            dashboardDao.deleteByMonitorId(monitorId);
+        } else {
+            String token = serviceAccountService.getToken();
+            String url = grafanaProperties.getPrefix() + 
grafanaProperties.getUrl() + String.format(DELETE_DASHBOARD_API, uid);
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            headers.setBearerAuth(token);
+
+            HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
+            dashboardDao.deleteByMonitorId(monitorId);
+
+            ResponseEntity<String> response = restTemplate.exchange(url, 
HttpMethod.DELETE, requestEntity, String.class);
+
+            if (response.getStatusCode().is2xxSuccessful()) {
+                log.info("delete dashboard success");
+            } else {
+                log.error("delete dashboard error: {}", 
response.getStatusCode());
+                throw new RuntimeException("delete dashboard error");
+            }
+        }
+    }
+
+
+    /**
+     * Retrieves a dashboard by monitor ID.
+     *
+     * @param monitorId the ID of the monitor associated with the dashboard
+     * @return GrafanaDashboard object
+     */
+    public GrafanaDashboard getDashboardByMonitorId(Long monitorId) {
+        return dashboardDao.findByMonitorId(monitorId);
+    }
+
+    /**
+     * Disables a Grafana dashboard by monitor ID.
+     *
+     * @param monitorId the ID of the monitor associated with the dashboard
+     */
+    public void closeGrafanaDashboard(Long monitorId) {
+        GrafanaDashboard grafanaDashboard = 
dashboardDao.findByMonitorId(monitorId);
+        if (grafanaDashboard != null) {
+            grafanaDashboard.setEnabled(false);
+            dashboardDao.save(grafanaDashboard);
+        }
+    }
+
+    /**
+     * Enables a Grafana dashboard by monitor ID.
+     *
+     * @param monitorId the ID of the monitor associated with the dashboard
+     */
+    public void openGrafanaDashboard(Long monitorId) {
+        GrafanaDashboard grafanaDashboard = 
dashboardDao.findByMonitorId(monitorId);
+        if (grafanaDashboard != null) {
+            grafanaDashboard.setEnabled(true);
+            dashboardDao.save(grafanaDashboard);
+        }
+    }
+}
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/service/DatasourceService.java
 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/service/DatasourceService.java
new file mode 100644
index 000000000..baebd1baf
--- /dev/null
+++ 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/service/DatasourceService.java
@@ -0,0 +1,134 @@
+/*
+ * 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.grafana.service;
+
+
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.CREATE_DATASOURCE_API;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.DATASOURCE_ACCESS;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.DATASOURCE_NAME;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.DATASOURCE_TYPE;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.DELETE_DATASOURCE_API;
+import jakarta.annotation.PostConstruct;
+import java.util.Base64;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.grafana.config.GrafanaProperties;
+import 
org.apache.hertzbeat.warehouse.store.history.vm.VictoriaMetricsProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Service for managing Grafana datasources.
+ */
+@Service
+@Slf4j
+public class DatasourceService {
+
+    private String grafanaUrl;
+    private String username;
+    private String password;
+    private String prefix;
+    private String victoriaMetricsUrl;
+
+    private final GrafanaProperties grafanaProperties;
+    private final VictoriaMetricsProperties warehouseProperties;
+    private final RestTemplate restTemplate;
+
+    @Autowired
+    public DatasourceService(
+            GrafanaProperties grafanaProperties,
+            VictoriaMetricsProperties warehouseProperties,
+            RestTemplate restTemplate
+    ) {
+        this.grafanaProperties = grafanaProperties;
+        this.warehouseProperties = warehouseProperties;
+        this.restTemplate = restTemplate;
+    }
+
+    @PostConstruct
+    public void init() {
+        this.grafanaUrl = grafanaProperties.getUrl();
+        this.username = grafanaProperties.username();
+        this.password = grafanaProperties.password();
+        this.prefix = grafanaProperties.getPrefix();
+        this.victoriaMetricsUrl = warehouseProperties.url();
+    }
+
+    /**
+     * Create a new datasource in Grafana.
+     */
+    public void createDatasource() {
+        String url = String.format(prefix + CREATE_DATASOURCE_API, username, 
password, grafanaUrl);
+
+        HttpHeaders headers = createHeaders();
+
+        String body = String.format(
+                
"{\"name\":\"%s\",\"type\":\"%s\",\"access\":\"%s\",\"url\":\"%s\",\"basicAuth\":%s}",
+                DATASOURCE_NAME, DATASOURCE_TYPE, DATASOURCE_ACCESS, 
victoriaMetricsUrl, false
+        );
+
+        HttpEntity<String> entity = new HttpEntity<>(body, headers);
+
+        try {
+            ResponseEntity<String> response = restTemplate.postForEntity(url, 
entity, String.class);
+            if (response.getStatusCode().is2xxSuccessful()) {
+                log.info("Create datasource success");
+            }
+        } catch (Exception ex) {
+            log.error("Create datasource error", ex);
+            throw new RuntimeException("Create datasource error", ex);
+        }
+    }
+
+    /**
+     * Delete a datasource in Grafana.
+     */
+    public void deleteDatasource() {
+        String url = String.format(prefix + DELETE_DATASOURCE_API, username, 
password, grafanaUrl, DATASOURCE_NAME);
+
+        HttpHeaders headers = createHeaders();
+
+        HttpEntity<Void> entity = new HttpEntity<>(headers);
+
+        try {
+            ResponseEntity<String> response = restTemplate.exchange(url, 
HttpMethod.DELETE, entity, String.class);
+            if (response.getStatusCode().is2xxSuccessful()) {
+                log.info("Delete datasource success");
+            }
+
+        } catch (Exception ex) {
+            log.error("Delete datasource error", ex);
+        }
+    }
+
+    private HttpHeaders createHeaders() {
+        String auth = username + ":" + password;
+        byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
+        String authHeader = "Basic " + new String(encodedAuth);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.set("Authorization", authHeader);
+        return headers;
+    }
+}
diff --git 
a/grafana/src/main/java/org/apache/hertzbeat/grafana/service/ServiceAccountService.java
 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/service/ServiceAccountService.java
new file mode 100644
index 000000000..355c621b9
--- /dev/null
+++ 
b/grafana/src/main/java/org/apache/hertzbeat/grafana/service/ServiceAccountService.java
@@ -0,0 +1,309 @@
+/*
+ * 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.grafana.service;
+
+import static org.apache.hertzbeat.grafana.common.CommonConstants.ACCOUNT_NAME;
+import static org.apache.hertzbeat.grafana.common.CommonConstants.ACCOUNT_ROLE;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.ACCOUNT_TOKEN_NAME;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.CREATE_SERVICE_ACCOUNT_API;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.CREATE_SERVICE_TOKEN_API;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.DELETE_SERVICE_ACCOUNT_API;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.GET_SERVICE_ACCOUNTS_API;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.GET_SERVICE_TOKENS_API;
+import static 
org.apache.hertzbeat.grafana.common.CommonConstants.HERTZBEAT_TOKEN;
+import com.fasterxml.jackson.databind.JsonNode;
+import jakarta.annotation.PostConstruct;
+import java.util.Base64;
+import java.util.List;
+import java.util.Objects;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.entity.grafana.ServiceAccount;
+import org.apache.hertzbeat.common.entity.grafana.ServiceToken;
+import org.apache.hertzbeat.common.util.JsonUtil;
+import org.apache.hertzbeat.grafana.config.GrafanaProperties;
+import org.apache.hertzbeat.grafana.dao.ServiceAccountDao;
+import org.apache.hertzbeat.grafana.dao.ServiceTokenDao;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+
+/**
+ * Service for managing Grafana service accounts and tokens.
+ */
+@Service
+@Slf4j
+public class ServiceAccountService {
+
+    private final GrafanaProperties grafanaProperties;
+    private final ServiceAccountDao serviceAccountDao;
+    private final ServiceTokenDao serviceTokenDao;
+    private final RestTemplate restTemplate;
+
+    private String url;
+    private String username;
+    private String password;
+    private String prefix;
+
+    @Autowired
+    public ServiceAccountService(
+            GrafanaProperties grafanaProperties,
+            ServiceAccountDao serviceAccountDao,
+            ServiceTokenDao serviceTokenDao,
+            RestTemplate restTemplate
+    ) {
+        this.grafanaProperties = grafanaProperties;
+        this.serviceAccountDao = serviceAccountDao;
+        this.serviceTokenDao = serviceTokenDao;
+        this.restTemplate = restTemplate;
+    }
+
+    @PostConstruct
+    public void init() {
+        this.url = grafanaProperties.getUrl();
+        this.username = grafanaProperties.username();
+        this.password = grafanaProperties.password();
+        this.prefix = grafanaProperties.getPrefix();
+        ServiceToken serviceToken = 
serviceTokenDao.findByName(ACCOUNT_TOKEN_NAME);
+        if (serviceToken == null) {
+            log.error("Service token {} not found", ACCOUNT_TOKEN_NAME);
+        }
+    }
+
+    /**
+     * Creates a new service admin account.
+     *
+     * @return ResponseEntity containing the result of the account creation
+     */
+    public ResponseEntity<String> createServiceAccount() {
+        String endpoint = String.format(prefix + CREATE_SERVICE_ACCOUNT_API, 
username, password, url);
+        HttpHeaders headers = createHeaders();
+        String body = 
String.format("{\"name\":\"%s\",\"role\":\"%s\",\"isDisabled\":false}", 
ACCOUNT_NAME, ACCOUNT_ROLE);
+
+        HttpEntity<String> request = new HttpEntity<>(body, headers);
+        try {
+            ResponseEntity<String> response = 
restTemplate.postForEntity(endpoint, request, String.class);
+            if (response.getStatusCode().is2xxSuccessful()) {
+                ServiceAccount serviceAccount = 
JsonUtil.fromJson(response.getBody(), ServiceAccount.class);
+                if (serviceAccount != null) {
+                    serviceAccountDao.save(serviceAccount);
+                    log.info("Create service account success: {}", 
serviceAccount);
+                }
+            }
+            return response;
+        } catch (Exception e) {
+            log.error("Service account creation failed", e);
+            throw new RuntimeException("Service account creation failed");
+        }
+    }
+
+    /**
+     * Deletes a service account by ID.
+     *
+     * @param id ID of the service account to delete
+     * @return ResponseEntity containing the result of the deletion
+     */
+    public ResponseEntity<String> deleteAccount(Long id) {
+        String endpoint = String.format(prefix + DELETE_SERVICE_ACCOUNT_API, 
username, password, url, id);
+        HttpHeaders headers = createHeaders();
+
+        HttpEntity<String> request = new HttpEntity<>(headers);
+        try {
+            ResponseEntity<String> response = restTemplate.exchange(endpoint, 
HttpMethod.DELETE, request, String.class);
+            if (response.getStatusCode().is2xxSuccessful()) {
+                log.info("Delete service account success");
+            }
+            return response;
+        } catch (Exception e) {
+            log.error("Delete service account error", e);
+            throw new RuntimeException("Delete service account error");
+        }
+    }
+
+    /**
+     * Creates a new API token for a service account.
+     *
+     * @return ResponseEntity containing the result of the token creation
+     */
+    public ResponseEntity<String> createToken() {
+        ServiceAccount hertzbeat = serviceAccountDao.findByName(ACCOUNT_NAME);
+        if (hertzbeat == null) {
+            log.error("Service account not found");
+            throw new RuntimeException("Service account not found");
+        }
+        String endpoint = String.format(prefix + CREATE_SERVICE_TOKEN_API, 
username, password, url, hertzbeat.getId());
+        HttpHeaders headers = createHeaders();
+        String body = String.format("{\"name\":\"%s\"}", HERTZBEAT_TOKEN);
+
+        HttpEntity<String> request = new HttpEntity<>(body, headers);
+        try {
+            ResponseEntity<String> response = 
restTemplate.postForEntity(endpoint, request, String.class);
+            if (response.getStatusCode().is2xxSuccessful()) {
+                ServiceToken serviceToken = 
JsonUtil.fromJson(response.getBody(), ServiceToken.class);
+                if (serviceToken != null) {
+                    serviceTokenDao.save(serviceToken);
+                }
+                log.info("Create token success: {}", response.getBody());
+            }
+            return response;
+        } catch (Exception e) {
+            log.error("Create token error", e);
+            throw new RuntimeException("Create token error");
+        }
+    }
+
+    /**
+     * Retrieves all service accounts.
+     *
+     * @return ResponseEntity containing the list of service accounts
+     */
+    public ResponseEntity<String> getAccounts() {
+        String endpoint = String.format(prefix + GET_SERVICE_ACCOUNTS_API, 
username, password, url);
+        HttpHeaders headers = createHeaders();
+
+        HttpEntity<String> request = new HttpEntity<>(headers);
+        try {
+            ResponseEntity<String> response = restTemplate.exchange(endpoint, 
HttpMethod.GET, request, String.class);
+            if (response.getStatusCode().is2xxSuccessful()) {
+                log.info("Get accounts success");
+            }
+            return response;
+        } catch (Exception e) {
+            log.error("Get accounts error", e);
+            throw new RuntimeException("Get accounts error");
+        }
+    }
+
+    /**
+     * Retrieves all API tokens for service accounts.
+     *
+     * @return ResponseEntity containing the list of tokens
+     */
+    public ResponseEntity<String> getTokens() {
+        String endpoint = String.format(prefix + GET_SERVICE_TOKENS_API, 
username, password, url, getAccountId());
+        HttpHeaders headers = createHeaders();
+
+        HttpEntity<String> request = new HttpEntity<>(headers);
+        try {
+            ResponseEntity<String> response = restTemplate.exchange(endpoint, 
HttpMethod.GET, request, String.class);
+            if (response.getStatusCode().is2xxSuccessful()) {
+                log.info("Get tokens success");
+            }
+            return response;
+        } catch (Exception e) {
+            log.error("Get tokens error", e);
+            throw new RuntimeException("Get tokens error");
+        }
+    }
+
+    /**
+     * Retrieves the token for a service account.
+     *
+     * @return The token key
+     */
+    public String getToken() {
+        ServiceToken hertzbeatToken = 
serviceTokenDao.findByName(ACCOUNT_TOKEN_NAME);
+        if (hertzbeatToken == null) {
+            log.error("Service token not found");
+            throw new RuntimeException("Service token not found");
+        }
+        return hertzbeatToken.getKey();
+    }
+
+    /**
+     * Deletes a service account token.
+     */
+    public void deleteToken() {
+        ServiceToken hertzbeatToken = 
serviceTokenDao.findByName(ACCOUNT_TOKEN_NAME);
+        if (hertzbeatToken == null) {
+            log.error("Service token not found");
+            throw new RuntimeException("Service token not found");
+        }
+        serviceTokenDao.delete(hertzbeatToken);
+    }
+
+    /**
+     * Retrieves the ID of the service account.
+     *
+     * @return The ID of the service account
+     */
+    public long getAccountId() {
+        ServiceAccount hertzbeat = serviceAccountDao.findByName(ACCOUNT_NAME);
+        if (hertzbeat == null) {
+            log.error("Service account not found");
+            throw new RuntimeException("Service account not found");
+        }
+        log.info("Service account: {}", hertzbeat);
+        return hertzbeat.getId();
+    }
+
+    /**
+     * Retrieves the service account.
+     *
+     * @return The service account
+     */
+    public ServiceAccount getAccount() {
+        ServiceAccount hertzbeat = serviceAccountDao.findByName(ACCOUNT_NAME);
+        if (hertzbeat == null) {
+            log.error("Service account not found");
+            throw new RuntimeException("Service account not found");
+        }
+        log.info("Service account: {}", hertzbeat);
+        return hertzbeat;
+    }
+
+    /**
+     * Deletes the service account.
+     */
+    public void deleteAccount() {
+        ServiceAccount hertzbeat = serviceAccountDao.findByName(ACCOUNT_NAME);
+        if (hertzbeat == null) {
+            log.error("Service account not found");
+            throw new RuntimeException("Service account not found");
+        }
+        serviceAccountDao.delete(hertzbeat);
+    }
+
+    /**
+     * Reloads the service accounts and tokens, clearing existing data.
+     */
+    public void reload() {
+        List<JsonNode> idList = 
Objects.requireNonNull(JsonUtil.fromJson(getAccounts().getBody())).path("serviceAccounts").findValues("id");
+        for (JsonNode jsonNode : idList) {
+            deleteAccount(jsonNode.asLong());
+        }
+        serviceAccountDao.truncate();
+        serviceTokenDao.truncate();
+    }
+
+    private HttpHeaders createHeaders() {
+        String auth = username + ":" + password;
+        byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
+        String authHeader = "Basic " + new String(encodedAuth);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.set("Authorization", authHeader);
+        return headers;
+    }
+}
\ No newline at end of file
diff --git a/grafana/src/main/resources/META-INF/spring.factories 
b/grafana/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..d621ea111
--- /dev/null
+++ b/grafana/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,17 @@
+# 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.
+
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+org.apache.hertzbeat.grafana.config.GrafanaAutoConfiguration
\ No newline at end of file
diff --git a/manager/pom.xml b/manager/pom.xml
index 34debeb78..63999336b 100644
--- a/manager/pom.xml
+++ b/manager/pom.xml
@@ -79,6 +79,11 @@
             <groupId>org.apache.hertzbeat</groupId>
             <artifactId>hertzbeat-plugin</artifactId>
         </dependency>
+        <!-- grafana -->
+        <dependency>
+            <groupId>org.apache.hertzbeat</groupId>
+            <artifactId>hertzbeat-grafana</artifactId>
+        </dependency>
         <!-- spring -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
diff --git 
a/manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
 
b/manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
index cd0ea218b..c11b708ee 100644
--- 
a/manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
+++ 
b/manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
@@ -61,6 +61,7 @@ import 
org.apache.hertzbeat.common.util.IntervalExpressionUtil;
 import org.apache.hertzbeat.common.util.IpDomainUtil;
 import org.apache.hertzbeat.common.util.JsonUtil;
 import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
+import org.apache.hertzbeat.grafana.service.DashboardService;
 import org.apache.hertzbeat.manager.dao.CollectorDao;
 import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao;
 import org.apache.hertzbeat.manager.dao.MonitorDao;
@@ -141,6 +142,9 @@ public class MonitorServiceImpl implements MonitorService {
     @Autowired
     private WarehouseService warehouseService;
 
+    @Autowired
+    private DashboardService dashboardService;
+
     private final Map<String, ImExportService> imExportServiceMap = new 
HashMap<>();
 
     public MonitorServiceImpl(List<ImExportService> imExportServiceList) {
@@ -231,6 +235,10 @@ public class MonitorServiceImpl implements MonitorService {
             }
             monitor.setId(monitorId);
             monitor.setJobId(jobId);
+            // create grafana dashboard
+            if (monitor.getApp().equals(CommonConstants.PROMETHEUS) && 
monitor.getGrafanaDashboard().isEnabled()) {
+                
dashboardService.createDashboard(monitor.getGrafanaDashboard().getTemplate(), 
monitorId);
+            }
             monitorDao.save(monitor);
             paramDao.saveAll(params);
         } catch (Exception e) {
@@ -564,6 +572,18 @@ public class MonitorServiceImpl implements MonitorService {
             }
             // force update gmtUpdate time, due the case: monitor not change, 
param change. we also think monitor change
             monitor.setGmtUpdate(LocalDateTime.now());
+            // create or open grafana dashboard
+            if (monitor.getApp().equals(CommonConstants.PROMETHEUS) && 
monitor.getGrafanaDashboard().isEnabled()) {
+                if (dashboardService.getDashboardByMonitorId(monitorId) == 
null) {
+                    
dashboardService.createDashboard(monitor.getGrafanaDashboard().getTemplate(), 
monitorId);
+                } else {
+                    dashboardService.openGrafanaDashboard(monitorId);
+                }
+            }
+            // close grafana dashboard
+            if (monitor.getApp().equals(CommonConstants.PROMETHEUS) && 
!monitor.getGrafanaDashboard().isEnabled()) {
+                dashboardService.closeGrafanaDashboard(monitorId);
+            }
             monitorDao.save(monitor);
             if (params != null) {
                 paramDao.saveAll(params);
@@ -621,13 +641,13 @@ public class MonitorServiceImpl implements MonitorService 
{
         if (monitorOptional.isPresent()) {
             Monitor monitor = monitorOptional.get();
             MonitorDto monitorDto = new MonitorDto();
-            monitorDto.setMonitor(monitor);
             List<Param> params = paramDao.findParamsByMonitorId(id);
             monitorDto.setParams(params);
             if 
(DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(monitor.getApp())) {
                 List<CollectRep.MetricsData> metricsDataList = 
warehouseService.queryMonitorMetricsData(id);
                 List<String> metrics = 
metricsDataList.stream().map(CollectRep.MetricsData::getMetrics).collect(Collectors.toList());
                 monitorDto.setMetrics(metrics);
+                
monitor.setGrafanaDashboard(dashboardService.getDashboardByMonitorId(id));
             } else {
                 Job job = appService.getAppDefine(monitor.getApp());
                 List<String> metrics = job.getMetrics().stream()
@@ -635,6 +655,7 @@ public class MonitorServiceImpl implements MonitorService {
                         .map(Metrics::getName).collect(Collectors.toList());
                 monitorDto.setMetrics(metrics);   
             }
+            monitorDto.setMonitor(monitor);
             Optional<CollectorMonitorBind> bindOptional = 
collectorMonitorBindDao.findCollectorMonitorBindByMonitorId(monitor.getId());
             bindOptional.ifPresent(bind -> 
monitorDto.setCollector(bind.getCollector()));
             return monitorDto;
diff --git a/manager/src/main/resources/application-test.yml 
b/manager/src/main/resources/application-test.yml
index 24c06555d..d7e6cfa88 100644
--- a/manager/src/main/resources/application-test.yml
+++ b/manager/src/main/resources/application-test.yml
@@ -86,6 +86,12 @@ scheduler:
     enabled: true
     port: 1158
 
+grafana:
+  enabled: false
+  url: http://127.0.0.1:3000
+  username: admin
+  password: admin
+
 # AI config
 # See the documentation for details : 
https://hertzbeat.apache.org/zh-cn/docs/help/aiConfig
 ai:
diff --git a/manager/src/main/resources/application.yml 
b/manager/src/main/resources/application.yml
index 33ad559a2..1d0d878d8 100644
--- a/manager/src/main/resources/application.yml
+++ b/manager/src/main/resources/application.yml
@@ -205,7 +205,12 @@ scheduler:
     enabled: true
     port: 1158
 
-# AI config
+grafana:
+  enabled: false
+  url: http://127.0.0.1:3000
+  username: admin
+  password: admin
+
 # See the documentation for details : 
https://hertzbeat.apache.org/zh-cn/docs/help/aiConfig
 ai:
   # AI Type:zhiPu、alibabaAi、kimiAi、sparkDesk
diff --git a/manager/src/main/resources/sureness.yml 
b/manager/src/main/resources/sureness.yml
index d96f2a331..9ebd316af 100644
--- a/manager/src/main/resources/sureness.yml
+++ b/manager/src/main/resources/sureness.yml
@@ -58,6 +58,10 @@ resourceRole:
   - /api/status/page/**===post===[admin,user]
   - /api/status/page/**===put===[admin,user]
   - /api/status/page/**===delete===[admin]
+  - /api/grafana/**===get===[admin,user,guest]
+  - /api/grafana/**===post===[admin,user]
+  - /api/grafana/**===put===[admin,user]
+  - /api/grafana/**===delete===[admin]
   - /api/bulletin/**===get===[admin,user,guest]
   - /api/bulletin/**===post===[admin,user]
   - /api/bulletin/**===put===[admin,user]
diff --git a/pom.xml b/pom.xml
index 1a3541176..ffd243e9d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -87,6 +87,7 @@
         <module>remoting</module>
         <module>push</module>
         <module>plugin</module>
+        <module>grafana</module>
     </modules>
 
     <properties>
@@ -214,6 +215,12 @@
                 <artifactId>hertzbeat-plugin</artifactId>
                 <version>${hertzbeat.version}</version>
             </dependency>
+            <!-- grafana -->
+            <dependency>
+                <groupId>org.apache.hertzbeat</groupId>
+                <artifactId>hertzbeat-grafana</artifactId>
+                <version>${hertzbeat.version}</version>
+            </dependency>
             <!-- spring -->
             <dependency>
                 <groupId>org.springframework.boot</groupId>
diff --git a/web-app/src/app/pojo/GrafanaDashboard.ts 
b/web-app/src/app/pojo/GrafanaDashboard.ts
new file mode 100644
index 000000000..c84592da2
--- /dev/null
+++ b/web-app/src/app/pojo/GrafanaDashboard.ts
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+// Grafana pojo
+export class GrafanaDashboard {
+  // is enabled
+  enabled: boolean = false;
+  // grafana template json
+  template!: string;
+  // dashboard url
+  url!: string;
+}
diff --git a/web-app/src/app/pojo/Monitor.ts b/web-app/src/app/pojo/Monitor.ts
index c6e05a51e..9a633515b 100644
--- a/web-app/src/app/pojo/Monitor.ts
+++ b/web-app/src/app/pojo/Monitor.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+import { GrafanaDashboard } from './GrafanaDashboard';
 import { Tag } from './Tag';
 
 export class Monitor {
@@ -33,4 +34,5 @@ export class Monitor {
   gmtCreate!: number;
   gmtUpdate!: number;
   tags!: Tag[];
+  grafanaDashboard!: GrafanaDashboard;
 }
diff --git a/web-app/src/app/routes/SafePipe.ts 
b/web-app/src/app/routes/SafePipe.ts
new file mode 100644
index 000000000..021fc0dca
--- /dev/null
+++ b/web-app/src/app/routes/SafePipe.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Pipe, PipeTransform } from '@angular/core';
+import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
+
+@Pipe({
+  standalone: true,
+  name: 'safe'
+})
+export class SafePipe implements PipeTransform {
+  constructor(private sanitizer: DomSanitizer) {}
+
+  transform(url: string, type: string): SafeResourceUrl {
+    return this.sanitizer.bypassSecurityTrustResourceUrl(url);
+  }
+}
diff --git 
a/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.html 
b/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.html
index 7cf402565..2c3b9d597 100755
--- 
a/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.html
+++ 
b/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.html
@@ -91,6 +91,15 @@
             ></app-monitor-data-chart>
           </div>
         </nz-tab>
+        <nz-tab *ngIf="monitor.grafanaDashboard.enabled" 
[nzTitle]="title3Template">
+          <ng-template #title3Template>
+            <i nz-icon nzType="line-chart" style="margin-left: 10px"></i>
+            Grafana
+          </ng-template>
+          <div style="width: 100%; height: 1200px; margin-bottom: 6px">
+            <iframe [src]="monitor.grafanaDashboard.url | safe : 
'resourceUrl'" width="100%" height="100%" style="border: none"> </iframe>
+          </div>
+        </nz-tab>
       </nz-tabset>
     </nz-spin>
   </nz-content>
diff --git 
a/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.ts 
b/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.ts
index 103b72b79..76a5cf089 100644
--- a/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.ts
+++ b/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.ts
@@ -63,6 +63,7 @@ export class MonitorDetailComponent implements OnInit, 
OnDestroy {
   ngOnInit(): void {
     this.countDownTime = this.deadline;
     this.loadRealTimeMetric();
+    this.getGrafana();
   }
 
   loadMetricChart() {
@@ -199,6 +200,21 @@ export class MonitorDetailComponent implements OnInit, 
OnDestroy {
     this.cdr.detectChanges();
   }
 
+  getGrafana() {
+    this.monitorSvc.getGrafanaDashboard(this.monitorId).subscribe(
+      message => {
+        if (message.code === 0 && message.msg != null) {
+          this.monitor.grafanaDashboard = message.data;
+        } else {
+          console.warn(message.msg);
+        }
+      },
+      error => {
+        console.error(error.msg);
+      }
+    );
+  }
+
   ngOnDestroy(): void {
     clearInterval(this.interval$);
   }
diff --git 
a/web-app/src/app/routes/monitor/monitor-edit/monitor-edit.component.ts 
b/web-app/src/app/routes/monitor/monitor-edit/monitor-edit.component.ts
index 867339855..30743394d 100644
--- a/web-app/src/app/routes/monitor/monitor-edit/monitor-edit.component.ts
+++ b/web-app/src/app/routes/monitor/monitor-edit/monitor-edit.component.ts
@@ -23,10 +23,12 @@ import { ActivatedRoute, ParamMap, Router } from 
'@angular/router';
 import { I18NService } from '@core';
 import { ALAIN_I18N_TOKEN, TitleService } from '@delon/theme';
 import { NzNotificationService } from 'ng-zorro-antd/notification';
+import { NzUploadFile } from 'ng-zorro-antd/upload';
 import { throwError } from 'rxjs';
 import { switchMap } from 'rxjs/operators';
 
 import { Collector } from '../../../pojo/Collector';
+import { GrafanaDashboard } from '../../../pojo/GrafanaDashboard';
 import { Message } from '../../../pojo/Message';
 import { Monitor } from '../../../pojo/Monitor';
 import { Param } from '../../../pojo/Param';
@@ -59,6 +61,7 @@ export class MonitorEditComponent implements OnInit {
   advancedParams!: Param[];
   paramValueMap!: Map<String, Param>;
   monitor = new Monitor();
+  grafanaDashboard!: GrafanaDashboard;
   collectors!: Collector[];
   collector: string = '';
   detected: boolean = false;
@@ -246,4 +249,18 @@ export class MonitorEditComponent implements OnInit {
     app = app ? app : '';
     this.router.navigateByUrl(`/monitors?app=${app}`);
   }
+
+  //start grafana
+  handleTemplateInput(event: any): any {
+    if (event.file && event.file.originFileObj) {
+      const fileReader = new FileReader();
+      fileReader.readAsText(event.file.originFileObj, 'UTF-8');
+      fileReader.onload = () => {
+        this.grafanaDashboard.template = fileReader.result as string;
+      };
+      fileReader.onerror = error => {
+        console.log(error);
+      };
+    }
+  }
 }
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 c96cf1197..e91c22d5b 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,39 @@
         </nz-form-control>
       </nz-form-item>
 
+      <nz-divider *ngIf="monitor.app === 'prometheus'"></nz-divider>
+      <!-- grafana is enabled -->
+      <nz-form-item *ngIf="monitor.app === 'prometheus'">
+        <nz-form-label nzSpan="7" nzFor="grafana" 
[nzTooltipTitle]="'monitor.grafana.enabled.tip' | i18n">
+          {{ 'monitor.grafana.enabled.label' | i18n }}
+        </nz-form-label>
+        <nz-form-control nzSpan="8">
+          <nz-switch [(ngModel)]="monitor.grafanaDashboard.enabled" 
name="grafana" id="grafana"></nz-switch>
+        </nz-form-control>
+      </nz-form-item>
+      <!-- upload grafana dashboard json -->
+      <nz-form-item *ngIf="monitor.grafanaDashboard.enabled && monitor.app === 
'prometheus'">
+        <nz-form-label nzSpan="7" nzFor="grafanaJson" 
[nzTooltipTitle]="'monitor.grafana.upload.tip' | i18n">
+          {{ 'monitor.grafana.upload.label' | i18n }}
+        </nz-form-label>
+        <nz-form-control nzSpan="8">
+          <nz-upload
+            nzName="file"
+            nzListType="text"
+            [nzShowUploadList]="true"
+            [nzMultiple]="false"
+            [nzLimit]="1"
+            [nzCustomRequest]="handleTemplateInput"
+            (nzChange)="handleTemplateInput($event)"
+          >
+            <button nz-button>
+              <i nz-icon nzType="upload"></i>
+              {{ 'monitor.grafana.upload.label' | i18n }}
+            </button>
+          </nz-upload>
+        </nz-form-control>
+      </nz-form-item>
+
       <div nz-row>
         <div nz-col [nzXs]="{ span: 24 }" [nzLg]="{ span: 8, offset: 7 }" 
style="text-align: center">
           <button
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 7c90e02df..6a931ec99 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
@@ -177,4 +177,18 @@ export class MonitorFormComponent implements OnChanges {
       }
     });
   }
+
+  //start grafana
+  handleTemplateInput(event: any): any {
+    if (event.file && event.file.originFileObj) {
+      const fileReader = new FileReader();
+      fileReader.readAsText(event.file.originFileObj, 'UTF-8');
+      fileReader.onload = () => {
+        this.monitor.grafanaDashboard.template = fileReader.result as string;
+      };
+      fileReader.onerror = error => {
+        console.log(error);
+      };
+    }
+  }
 }
diff --git 
a/web-app/src/app/routes/monitor/monitor-list/monitor-list.component.ts 
b/web-app/src/app/routes/monitor/monitor-list/monitor-list.component.ts
index a686d786e..1184a6bbd 100644
--- a/web-app/src/app/routes/monitor/monitor-list/monitor-list.component.ts
+++ b/web-app/src/app/routes/monitor/monitor-list/monitor-list.component.ts
@@ -312,6 +312,10 @@ export class MonitorListComponent implements OnInit, 
OnDestroy {
         this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), 
error.msg);
       }
     );
+    // delete grafana dashboard
+    for (let monitorId of monitors) {
+      this.deleteGrafanaDashboard(monitorId);
+    }
   }
 
   updatePageIndex(delSize: number) {
@@ -575,5 +579,20 @@ export class MonitorListComponent implements OnInit, 
OnDestroy {
 
   // end: app type search filter
 
+  deleteGrafanaDashboard(monitorId: number) {
+    this.monitorSvc.deleteGrafanaDashboard(monitorId).subscribe(
+      message => {
+        if (message.code === 0) {
+          console.log('delete grafana dashboard success');
+        } else {
+          console.warn(message.msg);
+        }
+      },
+      error => {
+        console.error(error.msg);
+      }
+    );
+  }
+
   protected readonly sliceTagName = formatTagName;
 }
diff --git 
a/web-app/src/app/routes/monitor/monitor-new/monitor-new.component.ts 
b/web-app/src/app/routes/monitor/monitor-new/monitor-new.component.ts
index 7dc647297..0a5e6c9d6 100644
--- a/web-app/src/app/routes/monitor/monitor-new/monitor-new.component.ts
+++ b/web-app/src/app/routes/monitor/monitor-new/monitor-new.component.ts
@@ -25,6 +25,7 @@ import { NzNotificationService } from 
'ng-zorro-antd/notification';
 import { switchMap } from 'rxjs/operators';
 
 import { Collector } from '../../../pojo/Collector';
+import { GrafanaDashboard } from '../../../pojo/GrafanaDashboard';
 import { Message } from '../../../pojo/Message';
 import { Monitor } from '../../../pojo/Monitor';
 import { Param } from '../../../pojo/Param';
@@ -64,6 +65,7 @@ export class MonitorNewComponent implements OnInit {
   ) {
     this.monitor = new Monitor();
     this.monitor.tags = [];
+    this.monitor.grafanaDashboard = new GrafanaDashboard();
   }
 
   ngOnInit(): void {
diff --git a/web-app/src/app/routes/monitor/monitor.module.ts 
b/web-app/src/app/routes/monitor/monitor.module.ts
index abef9b4cf..10e6f5bd2 100644
--- a/web-app/src/app/routes/monitor/monitor.module.ts
+++ b/web-app/src/app/routes/monitor/monitor.module.ts
@@ -32,6 +32,7 @@ import { NzTagModule } from 'ng-zorro-antd/tag';
 import { NzUploadModule } from 'ng-zorro-antd/upload';
 import { NgxEchartsModule } from 'ngx-echarts';
 
+import { SafePipe } from '../SafePipe';
 import { MonitorDataChartComponent } from 
'./monitor-data-chart/monitor-data-chart.component';
 import { MonitorDataTableComponent } from 
'./monitor-data-table/monitor-data-table.component';
 import { MonitorDetailComponent } from 
'./monitor-detail/monitor-detail.component';
@@ -66,6 +67,7 @@ const COMPONENTS: Array<Type<void>> = [
     NzCollapseModule,
     ClipboardModule,
     NzUploadModule,
+    SafePipe,
     NzListModule
   ],
   declarations: COMPONENTS
diff --git a/web-app/src/app/service/monitor.service.ts 
b/web-app/src/app/service/monitor.service.ts
index ba215cb51..c2771e8bb 100644
--- a/web-app/src/app/service/monitor.service.ts
+++ b/web-app/src/app/service/monitor.service.ts
@@ -32,6 +32,7 @@ const manage_monitors_uri = '/monitors/manage';
 const export_monitors_uri = '/monitors/export';
 const summary_uri = '/summary';
 const warehouse_storage_status_uri = '/warehouse/storage/status';
+const grafana_dashboard_uri = '/grafana/dashboard';
 
 @Injectable({
   providedIn: 'root'
@@ -184,4 +185,12 @@ export class MonitorService {
   public getWarehouseStorageServerStatus(): Observable<Message<any>> {
     return this.http.get<Message<any>>(warehouse_storage_status_uri);
   }
+
+  public getGrafanaDashboard(monitorId: number): Observable<Message<any>> {
+    return 
this.http.get<Message<any>>(`${grafana_dashboard_uri}?monitorId=${monitorId}`);
+  }
+
+  public deleteGrafanaDashboard(monitorId: number): Observable<Message<any>> {
+    return 
this.http.delete<Message<any>>(`${grafana_dashboard_uri}?monitorId=${monitorId}`);
+  }
 }
diff --git a/web-app/src/assets/i18n/en-US.json 
b/web-app/src/assets/i18n/en-US.json
index f7fc8ae19..15f972f0e 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -126,7 +126,13 @@
       "down": "Down",
       "unreachable": "Unreachable",
       "paused": "Paused"
-    }
+    },
+    "grafana": {
+      "enabled.tip": "is enabled, the monitoring data will be displayed in 
Grafana",
+      "enabled.label": "Enable Grafana",
+      "upload.tip": "Upload Grafana template file, support .json file",
+      "upload.label": "Upload Grafana Template"
+     }
   },
   "alert": {
     "": "Alert",
diff --git a/web-app/src/assets/i18n/zh-CN.json 
b/web-app/src/assets/i18n/zh-CN.json
index 45e7174ab..99482d15e 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -112,6 +112,12 @@
       "down": "宕机",
       "unreachable": "不可达",
       "paused": "暂停"
+    },
+    "grafana": {
+      "enabled.tip": "是否启用Grafana",
+      "enabled.label": "启用Grafana",
+      "upload.tip": "上传Grafana的JSON文件",
+      "upload.label": "上传Grafana模板"
     }
   },
   "alert": {
diff --git a/web-app/src/assets/i18n/zh-TW.json 
b/web-app/src/assets/i18n/zh-TW.json
index f52a0a9d3..d873c2fe8 100644
--- a/web-app/src/assets/i18n/zh-TW.json
+++ b/web-app/src/assets/i18n/zh-TW.json
@@ -126,7 +126,13 @@
       "down": "宕機",
       "unreachable": "不可達",
       "paused": "暫停"
-    }
+    },
+    "grafana": {
+      "enabled.tip": "是否啓用Grafana監控",
+      "enabled.label": "啓用Grafana監控",
+      "upload.tip": "上傳Grafana模板文件",
+      "upload.label": "上傳Grafana模板"
+     }
   },
   "alert": {
     "": "告警",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to