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

HTHou pushed a commit to branch codex/prometheus
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit 283b6fe3e42679cd89c7eb8a1176040ef790863a
Author: HTHou <[email protected]>
AuthorDate: Fri May 8 12:33:54 2026 +0800

    Add metric scrape service to distribution
---
 distribution/pom.xml                               |   6 +
 distribution/src/assembly/all.xml                  |   4 +
 distribution/src/assembly/datanode.xml             |   4 +
 .../src/assembly/external-service-impl.xml         |   4 +
 external-service-impl/metric-scrape/pom.xml        |  99 ++++++++
 .../iotdb/metricscrape/MetricScrapeConfig.java     | 122 +++++++++
 .../iotdb/metricscrape/MetricScrapeHttpClient.java |  60 +++++
 .../iotdb/metricscrape/MetricScrapeService.java    | 125 +++++++++
 .../iotdb/metricscrape/MetricScrapeTarget.java     |  38 +++
 .../iotdb/metricscrape/MetricTableWriter.java      | 280 +++++++++++++++++++++
 .../iotdb/metricscrape/PrometheusSample.java       |  56 +++++
 .../iotdb/metricscrape/PrometheusTextParser.java   | 255 +++++++++++++++++++
 .../metricscrape/PrometheusTextParserTest.java     |  91 +++++++
 external-service-impl/pom.xml                      |   1 +
 .../java/org/apache/iotdb/db/conf/IoTDBConfig.java |  66 +++++
 .../org/apache/iotdb/db/conf/IoTDBDescriptor.java  |  37 +++
 .../externalservice/BuiltinExternalServices.java   |   6 +-
 .../conf/iotdb-system.properties.template          |  34 +++
 .../apache/iotdb/commons/conf/IoTDBConstant.java   |   8 +
 19 files changed, 1295 insertions(+), 1 deletion(-)

diff --git a/distribution/pom.xml b/distribution/pom.xml
index f4773f6afad..22c862aef62 100644
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -59,6 +59,12 @@
             <version>2.0.7-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.iotdb</groupId>
+            <artifactId>metric-scrape</artifactId>
+            <version>2.0.7-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.iotdb</groupId>
             <artifactId>rest</artifactId>
diff --git a/distribution/src/assembly/all.xml 
b/distribution/src/assembly/all.xml
index 1b2e1be054f..4a5d820bfde 100644
--- a/distribution/src/assembly/all.xml
+++ b/distribution/src/assembly/all.xml
@@ -96,6 +96,10 @@
             
<source>${maven.multiModuleProjectDirectory}/external-service-impl/mqtt/target/mqtt-${project.version}-jar-with-dependencies.jar</source>
             <outputDirectory>lib</outputDirectory>
         </file>
+        <file>
+            
<source>${maven.multiModuleProjectDirectory}/external-service-impl/metric-scrape/target/metric-scrape-${project.version}-jar-with-dependencies.jar</source>
+            <outputDirectory>lib</outputDirectory>
+        </file>
         <file>
             
<source>${maven.multiModuleProjectDirectory}/external-service-impl/rest/target/rest-${project.version}-jar-with-dependencies.jar</source>
             <outputDirectory>lib</outputDirectory>
diff --git a/distribution/src/assembly/datanode.xml 
b/distribution/src/assembly/datanode.xml
index 225fa5a7e7d..65e0e95ec60 100644
--- a/distribution/src/assembly/datanode.xml
+++ b/distribution/src/assembly/datanode.xml
@@ -79,6 +79,10 @@
             
<source>${maven.multiModuleProjectDirectory}/external-service-impl/mqtt/target/mqtt-${project.version}-jar-with-dependencies.jar</source>
             <outputDirectory>lib</outputDirectory>
         </file>
+        <file>
+            
<source>${maven.multiModuleProjectDirectory}/external-service-impl/metric-scrape/target/metric-scrape-${project.version}-jar-with-dependencies.jar</source>
+            <outputDirectory>lib</outputDirectory>
+        </file>
         <file>
             
<source>${maven.multiModuleProjectDirectory}/external-service-impl/rest/target/rest-${project.version}-jar-with-dependencies.jar</source>
             <outputDirectory>lib</outputDirectory>
diff --git a/distribution/src/assembly/external-service-impl.xml 
b/distribution/src/assembly/external-service-impl.xml
index c743fa85597..ff8098cdf0a 100644
--- a/distribution/src/assembly/external-service-impl.xml
+++ b/distribution/src/assembly/external-service-impl.xml
@@ -47,6 +47,10 @@
             
<source>${maven.multiModuleProjectDirectory}/external-service-impl/mqtt/target/mqtt-${project.version}-jar-with-dependencies.jar</source>
             <outputDirectory>/</outputDirectory>
         </file>
+        <file>
+            
<source>${maven.multiModuleProjectDirectory}/external-service-impl/metric-scrape/target/metric-scrape-${project.version}-jar-with-dependencies.jar</source>
+            <outputDirectory>/</outputDirectory>
+        </file>
         <file>
             
<source>${maven.multiModuleProjectDirectory}/external-service-impl/rest/target/rest-${project.version}-jar-with-dependencies.jar</source>
             <outputDirectory>/</outputDirectory>
diff --git a/external-service-impl/metric-scrape/pom.xml 
b/external-service-impl/metric-scrape/pom.xml
new file mode 100644
index 00000000000..4e956f55f97
--- /dev/null
+++ b/external-service-impl/metric-scrape/pom.xml
@@ -0,0 +1,99 @@
+<?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 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.iotdb</groupId>
+        <artifactId>external-service-impl</artifactId>
+        <version>2.0.7-SNAPSHOT</version>
+    </parent>
+    <artifactId>metric-scrape</artifactId>
+    <name>IoTDB: External-Service-Impl: Metric Scrape</name>
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.iotdb</groupId>
+            <artifactId>node-commons</artifactId>
+            <version>2.0.7-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.iotdb</groupId>
+            <artifactId>iotdb-server</artifactId>
+            <version>2.0.7-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.tsfile</groupId>
+            <artifactId>tsfile</artifactId>
+            <version>${tsfile.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.tsfile</groupId>
+            <artifactId>common</artifactId>
+            <version>${tsfile.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>${maven.assembly.version}</version>
+                <configuration>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <phase>package</phase>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <configuration>
+                    <ignoredDependencies>
+                        
<ignoredDependency>org.apache.tsfile:common</ignoredDependency>
+                        <ignoredDependency>junit:junit</ignoredDependency>
+                    </ignoredDependencies>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git 
a/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeConfig.java
 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeConfig.java
new file mode 100644
index 00000000000..2ea52c7f91e
--- /dev/null
+++ 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeConfig.java
@@ -0,0 +1,122 @@
+/*
+ * 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.iotdb.metricscrape;
+
+import org.apache.iotdb.db.conf.IoTDBConfig;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class MetricScrapeConfig {
+
+  private final List<MetricScrapeTarget> targets;
+  private final int intervalSeconds;
+  private final String database;
+  private final String table;
+  private final int httpTimeoutMs;
+
+  private MetricScrapeConfig(
+      List<MetricScrapeTarget> targets,
+      int intervalSeconds,
+      String database,
+      String table,
+      int httpTimeoutMs) {
+    this.targets = targets;
+    this.intervalSeconds = intervalSeconds;
+    this.database = database;
+    this.table = table;
+    this.httpTimeoutMs = httpTimeoutMs;
+  }
+
+  public static MetricScrapeConfig from(IoTDBConfig config) {
+    if (config.getMetricScrapeIntervalSeconds() <= 0) {
+      throw new IllegalArgumentException("metric_scrape_interval_seconds 
should be positive");
+    }
+    if (config.getMetricScrapeHttpTimeoutMs() <= 0) {
+      throw new IllegalArgumentException("metric_scrape_http_timeout_ms should 
be positive");
+    }
+    if (config.getMetricScrapeDatabase() == null
+        || config.getMetricScrapeDatabase().trim().isEmpty()) {
+      throw new IllegalArgumentException("metric_scrape_database should not be 
empty");
+    }
+    if (config.getMetricScrapeTable() == null || 
config.getMetricScrapeTable().trim().isEmpty()) {
+      throw new IllegalArgumentException("metric_scrape_table should not be 
empty");
+    }
+    return new MetricScrapeConfig(
+        parseTargets(config.getMetricScrapeTargets()),
+        config.getMetricScrapeIntervalSeconds(),
+        config.getMetricScrapeDatabase().trim(),
+        config.getMetricScrapeTable().trim(),
+        config.getMetricScrapeHttpTimeoutMs());
+  }
+
+  private static List<MetricScrapeTarget> parseTargets(String rawTargets) {
+    if (rawTargets == null || rawTargets.trim().isEmpty()) {
+      return Collections.emptyList();
+    }
+    List<MetricScrapeTarget> targets = new ArrayList<>();
+    String[] targetItems = rawTargets.split(",");
+    for (String targetItem : targetItems) {
+      String target = targetItem.trim();
+      if (target.isEmpty()) {
+        continue;
+      }
+      validateTarget(target);
+      targets.add(new MetricScrapeTarget(target));
+    }
+    return Collections.unmodifiableList(targets);
+  }
+
+  private static void validateTarget(String target) {
+    try {
+      URL url = new URL(target);
+      if (!"http".equalsIgnoreCase(url.getProtocol())
+          && !"https".equalsIgnoreCase(url.getProtocol())) {
+        throw new IllegalArgumentException(
+            "Metric scrape target only supports http and https: " + target);
+      }
+    } catch (MalformedURLException e) {
+      throw new IllegalArgumentException("Illegal metric scrape target: " + 
target, e);
+    }
+  }
+
+  public List<MetricScrapeTarget> getTargets() {
+    return targets;
+  }
+
+  public int getIntervalSeconds() {
+    return intervalSeconds;
+  }
+
+  public String getDatabase() {
+    return database;
+  }
+
+  public String getTable() {
+    return table;
+  }
+
+  public int getHttpTimeoutMs() {
+    return httpTimeoutMs;
+  }
+}
diff --git 
a/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeHttpClient.java
 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeHttpClient.java
new file mode 100644
index 00000000000..4153afe4cb4
--- /dev/null
+++ 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeHttpClient.java
@@ -0,0 +1,60 @@
+/*
+ * 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.iotdb.metricscrape;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+public class MetricScrapeHttpClient {
+
+  public String get(String url, int timeoutMs) throws IOException {
+    HttpURLConnection connection = (HttpURLConnection) new 
URL(url).openConnection();
+    connection.setRequestMethod("GET");
+    connection.setConnectTimeout(timeoutMs);
+    connection.setReadTimeout(timeoutMs);
+    connection.setRequestProperty(
+        "Accept", "text/plain; version=0.0.4, application/openmetrics-text, 
*/*");
+    connection.setRequestProperty("User-Agent", "IoTDB-MetricScrapeService");
+
+    int statusCode = connection.getResponseCode();
+    if (statusCode < 200 || statusCode >= 300) {
+      throw new IOException("Metric scrape target " + url + " returns HTTP " + 
statusCode);
+    }
+    try (InputStream inputStream = connection.getInputStream()) {
+      return readToString(inputStream);
+    } finally {
+      connection.disconnect();
+    }
+  }
+
+  private String readToString(InputStream inputStream) throws IOException {
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    byte[] buffer = new byte[8192];
+    int length;
+    while ((length = inputStream.read(buffer)) != -1) {
+      outputStream.write(buffer, 0, length);
+    }
+    return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
+  }
+}
diff --git 
a/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeService.java
 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeService.java
new file mode 100644
index 00000000000..da1c98d93e4
--- /dev/null
+++ 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeService.java
@@ -0,0 +1,125 @@
+/*
+ * 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.iotdb.metricscrape;
+
+import org.apache.iotdb.commons.concurrent.threadpool.ScheduledExecutorUtil;
+import org.apache.iotdb.commons.utils.CommonDateTimeUtils;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.externalservice.api.IExternalService;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class MetricScrapeService implements IExternalService {
+
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(MetricScrapeService.class);
+
+  private final PrometheusTextParser parser = new PrometheusTextParser();
+  private final MetricScrapeHttpClient httpClient = new 
MetricScrapeHttpClient();
+
+  private ScheduledExecutorService executorService;
+  private MetricTableWriter writer;
+
+  @Override
+  public synchronized void start() {
+    if (executorService != null) {
+      return;
+    }
+
+    MetricScrapeConfig config = 
MetricScrapeConfig.from(IoTDBDescriptor.getInstance().getConfig());
+    if (config.getTargets().isEmpty()) {
+      LOGGER.warn("Metric scrape service is enabled but metric_scrape_targets 
is empty.");
+      return;
+    }
+
+    writer = new MetricTableWriter(config.getDatabase(), config.getTable());
+    writer.initializeSchema();
+
+    executorService =
+        Executors.newScheduledThreadPool(
+            config.getTargets().size(), new MetricScrapeThreadFactory());
+    for (MetricScrapeTarget target : config.getTargets()) {
+      ScheduledExecutorUtil.safelyScheduleWithFixedDelay(
+          executorService,
+          () -> scrapeTarget(target, config),
+          0,
+          config.getIntervalSeconds(),
+          TimeUnit.SECONDS);
+    }
+
+    LOGGER.info(
+        "Start MetricScrapeService successfully, targets={}, interval={}s, 
table={}.{}",
+        config.getTargets(),
+        config.getIntervalSeconds(),
+        config.getDatabase(),
+        config.getTable());
+  }
+
+  @Override
+  public synchronized void stop() {
+    if (executorService != null) {
+      executorService.shutdownNow();
+      executorService = null;
+    }
+    if (writer != null) {
+      writer.close();
+      writer = null;
+    }
+    LOGGER.info("MetricScrapeService stopped.");
+  }
+
+  private void scrapeTarget(MetricScrapeTarget target, MetricScrapeConfig 
config) {
+    try {
+      MetricTableWriter currentWriter = writer;
+      if (currentWriter == null) {
+        return;
+      }
+      long scrapeTimestamp = CommonDateTimeUtils.currentTime();
+      String prometheusText = httpClient.get(target.getUrl(), 
config.getHttpTimeoutMs());
+      List<PrometheusSample> samples = parser.parse(prometheusText, 
scrapeTimestamp);
+      if (samples.isEmpty()) {
+        LOGGER.debug("Metric scrape target {} returns no samples.", target);
+        return;
+      }
+      currentWriter.write(samples);
+      LOGGER.debug("Metric scrape target {} writes {} samples.", target, 
samples.size());
+    } catch (Exception e) {
+      LOGGER.warn("Failed to scrape metric target {}.", target, e);
+    }
+  }
+
+  private static class MetricScrapeThreadFactory implements ThreadFactory {
+    private final AtomicInteger index = new AtomicInteger();
+
+    @Override
+    public Thread newThread(Runnable runnable) {
+      Thread thread = new Thread(runnable, "metric-scrape-service-" + 
index.incrementAndGet());
+      thread.setDaemon(true);
+      return thread;
+    }
+  }
+}
diff --git 
a/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeTarget.java
 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeTarget.java
new file mode 100644
index 00000000000..88dec77c6d1
--- /dev/null
+++ 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricScrapeTarget.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.iotdb.metricscrape;
+
+public class MetricScrapeTarget {
+
+  private final String url;
+
+  public MetricScrapeTarget(String url) {
+    this.url = url;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  @Override
+  public String toString() {
+    return url;
+  }
+}
diff --git 
a/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricTableWriter.java
 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricTableWriter.java
new file mode 100644
index 00000000000..514c6e29518
--- /dev/null
+++ 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/MetricTableWriter.java
@@ -0,0 +1,280 @@
+/*
+ * 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.iotdb.metricscrape;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.conf.IoTDBConstant.ClientVersion;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.queryengine.common.SqlDialect;
+import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement;
+import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
+import org.apache.iotdb.db.auth.AuthorityChecker;
+import org.apache.iotdb.db.conf.IoTDBConfig;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.protocol.session.IClientSession;
+import org.apache.iotdb.db.protocol.session.InternalClientSession;
+import org.apache.iotdb.db.protocol.session.SessionManager;
+import org.apache.iotdb.db.queryengine.plan.Coordinator;
+import org.apache.iotdb.db.queryengine.plan.execution.ExecutionResult;
+import org.apache.iotdb.db.queryengine.plan.planner.LocalExecutionPlanner;
+import org.apache.iotdb.db.queryengine.plan.relational.metadata.Metadata;
+import org.apache.iotdb.db.queryengine.plan.relational.sql.parser.SqlParser;
+import 
org.apache.iotdb.db.queryengine.plan.statement.crud.InsertTabletStatement;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.utils.Binary;
+import org.apache.tsfile.utils.BitMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MetricTableWriter {
+
+  public static final String METRIC_NAME_COLUMN = "metric_name";
+  public static final String VALUE_COLUMN = "value";
+  private static final String TIME_COLUMN = "time";
+
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(MetricTableWriter.class);
+
+  private static final Coordinator COORDINATOR = Coordinator.getInstance();
+  private static final SessionManager SESSION_MANAGER = 
SessionManager.getInstance();
+  private static final IoTDBConfig IOTDB_CONFIG = 
IoTDBDescriptor.getInstance().getConfig();
+
+  private final String database;
+  private final String table;
+  private final IClientSession session;
+  private final SqlParser sqlParser = new SqlParser();
+  private final Metadata metadata = 
LocalExecutionPlanner.getInstance().metadata;
+
+  public MetricTableWriter(String database, String table) {
+    this.database = database;
+    this.table = table;
+    this.session = new InternalClientSession("METRIC_SCRAPE");
+    this.session.setDatabaseName(database);
+    this.session.setSqlDialect(SqlDialect.TABLE);
+    SESSION_MANAGER.supplySession(
+        session,
+        AuthorityChecker.SUPER_USER_ID,
+        AuthorityChecker.SUPER_USER,
+        ZoneId.systemDefault(),
+        ClientVersion.V_1_0);
+  }
+
+  public synchronized void initializeSchema() {
+    executeStatement("CREATE DATABASE IF NOT EXISTS " + 
quoteIdentifier(database));
+    session.setDatabaseName(database);
+    executeStatement(
+        "CREATE TABLE IF NOT EXISTS "
+            + quoteIdentifier(table)
+            + " ("
+            + METRIC_NAME_COLUMN
+            + " STRING TAG, "
+            + VALUE_COLUMN
+            + " DOUBLE FIELD)");
+  }
+
+  public synchronized void write(List<PrometheusSample> samples) {
+    if (samples.isEmpty()) {
+      return;
+    }
+    Map<List<String>, TabletBuilder> builders = new LinkedHashMap<>();
+    for (PrometheusSample sample : samples) {
+      List<String> labelKeys = new ArrayList<>(sample.getLabels().keySet());
+      Collections.sort(labelKeys);
+      validateLabelKeys(labelKeys);
+      TabletBuilder builder = builders.get(labelKeys);
+      if (builder == null) {
+        builder = new TabletBuilder(labelKeys);
+        builders.put(labelKeys, builder);
+      }
+      builder.add(sample);
+    }
+
+    for (TabletBuilder builder : builders.values()) {
+      executeInsert(builder.build());
+    }
+  }
+
+  public void close() {
+    SESSION_MANAGER.closeSession(session, COORDINATOR::cleanupQueryExecution, 
false);
+  }
+
+  private void validateLabelKeys(List<String> labelKeys) {
+    for (String labelKey : labelKeys) {
+      if (METRIC_NAME_COLUMN.equalsIgnoreCase(labelKey)
+          || VALUE_COLUMN.equalsIgnoreCase(labelKey)
+          || TIME_COLUMN.equalsIgnoreCase(labelKey)) {
+        throw new IllegalArgumentException(
+            "Prometheus label conflicts with reserved IoTDB table column name: 
" + labelKey);
+      }
+    }
+  }
+
+  private void executeInsert(InsertTabletStatement statement) {
+    long queryId = SESSION_MANAGER.requestQueryId();
+    try {
+      ExecutionResult result =
+          COORDINATOR.executeForTableModel(
+              statement,
+              sqlParser,
+              session,
+              queryId,
+              SESSION_MANAGER.getSessionInfo(session),
+              "",
+              metadata,
+              IOTDB_CONFIG.getQueryTimeoutThreshold());
+      warnIfFailed(result.status);
+    } finally {
+      COORDINATOR.cleanupQueryExecution(queryId);
+    }
+  }
+
+  private void executeStatement(String sql) {
+    long queryId = SESSION_MANAGER.requestQueryId();
+    try {
+      Statement statement = sqlParser.createStatement(sql, 
session.getZoneId(), session);
+      ExecutionResult result =
+          COORDINATOR.executeForTableModel(
+              statement,
+              sqlParser,
+              session,
+              queryId,
+              SESSION_MANAGER.getSessionInfo(session),
+              sql,
+              metadata,
+              IOTDB_CONFIG.getQueryTimeoutThreshold(),
+              false);
+      TSStatus status = result.status;
+      if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()
+          && status.getCode() != 
TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode()) {
+        throw new IllegalStateException(
+            "Failed to execute metric scrape schema statement: " + sql + ", 
status: " + status);
+      }
+    } finally {
+      COORDINATOR.cleanupQueryExecution(queryId);
+    }
+  }
+
+  private void warnIfFailed(TSStatus status) {
+    if (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()
+        || status.getCode() == 
TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode()) {
+      return;
+    }
+    LOGGER.warn(
+        "Metric scrape write failed, code={}, message={}", status.getCode(), 
status.getMessage());
+  }
+
+  private static String quoteIdentifier(String identifier) {
+    return "\"" + identifier.replace("\"", "\"\"") + "\"";
+  }
+
+  private class TabletBuilder {
+    private final List<String> labelKeys;
+    private final List<PrometheusSample> samples = new ArrayList<>();
+
+    private TabletBuilder(List<String> labelKeys) {
+      this.labelKeys = new ArrayList<>(labelKeys);
+    }
+
+    private void add(PrometheusSample sample) {
+      samples.add(sample);
+    }
+
+    private InsertTabletStatement build() {
+      InsertTabletStatement statement = new InsertTabletStatement();
+      statement.setDevicePath(new PartialPath(table, false));
+      statement.setWriteToTable(true);
+      statement.setAligned(false);
+      statement.setRowCount(samples.size());
+
+      List<String> measurements = new ArrayList<>(labelKeys.size() + 2);
+      measurements.add(METRIC_NAME_COLUMN);
+      measurements.addAll(labelKeys);
+      measurements.add(VALUE_COLUMN);
+      statement.setMeasurements(measurements.toArray(new String[0]));
+      statement.setTimes(buildTimes());
+      statement.setDataTypes(buildDataTypes(measurements.size()));
+      
statement.setColumnCategories(buildColumnCategories(measurements.size()));
+      statement.setColumns(buildColumns());
+      statement.setBitMaps(new BitMap[measurements.size()]);
+      return statement;
+    }
+
+    private long[] buildTimes() {
+      long[] times = new long[samples.size()];
+      for (int i = 0; i < samples.size(); i++) {
+        times[i] = samples.get(i).getTimestamp();
+      }
+      return times;
+    }
+
+    private TSDataType[] buildDataTypes(int columnSize) {
+      TSDataType[] dataTypes = new TSDataType[columnSize];
+      for (int i = 0; i < columnSize - 1; i++) {
+        dataTypes[i] = TSDataType.STRING;
+      }
+      dataTypes[columnSize - 1] = TSDataType.DOUBLE;
+      return dataTypes;
+    }
+
+    private TsTableColumnCategory[] buildColumnCategories(int columnSize) {
+      TsTableColumnCategory[] columnCategories = new 
TsTableColumnCategory[columnSize];
+      for (int i = 0; i < columnSize - 1; i++) {
+        columnCategories[i] = TsTableColumnCategory.TAG;
+      }
+      columnCategories[columnSize - 1] = TsTableColumnCategory.FIELD;
+      return columnCategories;
+    }
+
+    private Object[] buildColumns() {
+      Object[] columns = new Object[labelKeys.size() + 2];
+      columns[0] = buildStringColumn(METRIC_NAME_COLUMN);
+      for (int i = 0; i < labelKeys.size(); i++) {
+        columns[i + 1] = buildStringColumn(labelKeys.get(i));
+      }
+      double[] values = new double[samples.size()];
+      for (int i = 0; i < samples.size(); i++) {
+        values[i] = samples.get(i).getValue();
+      }
+      columns[columns.length - 1] = values;
+      return columns;
+    }
+
+    private Binary[] buildStringColumn(String columnName) {
+      Binary[] values = new Binary[samples.size()];
+      for (int i = 0; i < samples.size(); i++) {
+        String value =
+            METRIC_NAME_COLUMN.equals(columnName)
+                ? samples.get(i).getMetricName()
+                : samples.get(i).getLabels().get(columnName);
+        values[i] = new Binary(value.getBytes(StandardCharsets.UTF_8));
+      }
+      return values;
+    }
+  }
+}
diff --git 
a/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/PrometheusSample.java
 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/PrometheusSample.java
new file mode 100644
index 00000000000..664429f792a
--- /dev/null
+++ 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/PrometheusSample.java
@@ -0,0 +1,56 @@
+/*
+ * 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.iotdb.metricscrape;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class PrometheusSample {
+
+  private final String metricName;
+  private final Map<String, String> labels;
+  private final double value;
+  private final long timestamp;
+
+  public PrometheusSample(
+      String metricName, Map<String, String> labels, double value, long 
timestamp) {
+    this.metricName = metricName;
+    this.labels = Collections.unmodifiableMap(new LinkedHashMap<>(labels));
+    this.value = value;
+    this.timestamp = timestamp;
+  }
+
+  public String getMetricName() {
+    return metricName;
+  }
+
+  public Map<String, String> getLabels() {
+    return labels;
+  }
+
+  public double getValue() {
+    return value;
+  }
+
+  public long getTimestamp() {
+    return timestamp;
+  }
+}
diff --git 
a/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/PrometheusTextParser.java
 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/PrometheusTextParser.java
new file mode 100644
index 00000000000..949b0901243
--- /dev/null
+++ 
b/external-service-impl/metric-scrape/src/main/java/org/apache/iotdb/metricscrape/PrometheusTextParser.java
@@ -0,0 +1,255 @@
+/*
+ * 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.iotdb.metricscrape;
+
+import org.apache.iotdb.commons.queryengine.utils.TimestampPrecisionUtils;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public class PrometheusTextParser {
+
+  public List<PrometheusSample> parse(String text, long defaultTimestamp) {
+    if (text == null) {
+      throw new IllegalArgumentException("Prometheus text should not be null");
+    }
+    List<PrometheusSample> samples = new ArrayList<>();
+    String[] lines = text.split("\\r\\n|\\n|\\r");
+    for (int i = 0; i < lines.length; i++) {
+      String line = lines[i].trim();
+      if (line.isEmpty() || line.startsWith("#")) {
+        continue;
+      }
+      samples.add(parseSample(line, i + 1, defaultTimestamp));
+    }
+    return samples;
+  }
+
+  private PrometheusSample parseSample(String line, int lineNumber, long 
defaultTimestamp) {
+    Parser parser = new Parser(line, lineNumber);
+    String metricName = parser.parseMetricName();
+    Map<String, String> labels = parser.parseLabels();
+    parser.skipWhitespace();
+    double value = parser.parseValue();
+    parser.skipWhitespace();
+    long timestamp = parser.parseOptionalTimestamp(defaultTimestamp);
+    parser.skipWhitespace();
+    parser.expectEnd();
+    return new PrometheusSample(metricName, labels, value, timestamp);
+  }
+
+  private static double parsePrometheusDouble(String token, int lineNumber) {
+    if ("+Inf".equals(token) || "Inf".equals(token)) {
+      return Double.POSITIVE_INFINITY;
+    }
+    if ("-Inf".equals(token)) {
+      return Double.NEGATIVE_INFINITY;
+    }
+    try {
+      return Double.parseDouble(token);
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException(
+          "Illegal Prometheus sample value " + token + " at line " + 
lineNumber, e);
+    }
+  }
+
+  private static long convertTimestamp(long prometheusTimestampMillis, int 
lineNumber) {
+    long timestamp =
+        TimestampPrecisionUtils.convertToCurrPrecision(
+            prometheusTimestampMillis, TimeUnit.MILLISECONDS);
+    try {
+      TimestampPrecisionUtils.checkTimestampPrecision(timestamp);
+    } catch (RuntimeException e) {
+      throw new IllegalArgumentException(
+          "Illegal Prometheus sample timestamp "
+              + prometheusTimestampMillis
+              + " at line "
+              + lineNumber,
+          e);
+    }
+    return timestamp;
+  }
+
+  private static class Parser {
+    private final String line;
+    private final int lineNumber;
+    private int position;
+
+    private Parser(String line, int lineNumber) {
+      this.line = line;
+      this.lineNumber = lineNumber;
+    }
+
+    private String parseMetricName() {
+      if (position >= line.length() || 
Character.isWhitespace(line.charAt(position))) {
+        throw new IllegalArgumentException("Illegal Prometheus metric name at 
line " + lineNumber);
+      }
+      int start = position;
+      while (position < line.length()
+          && !Character.isWhitespace(line.charAt(position))
+          && line.charAt(position) != '{') {
+        position++;
+      }
+      if (start == position) {
+        throw new IllegalArgumentException("Illegal Prometheus metric name at 
line " + lineNumber);
+      }
+      return line.substring(start, position);
+    }
+
+    private Map<String, String> parseLabels() {
+      Map<String, String> labels = new LinkedHashMap<>();
+      if (position >= line.length() || line.charAt(position) != '{') {
+        return labels;
+      }
+      position++;
+      skipWhitespace();
+      if (position < line.length() && line.charAt(position) == '}') {
+        position++;
+        return labels;
+      }
+
+      while (position < line.length()) {
+        String name = parseLabelName();
+        skipWhitespace();
+        expect('=');
+        skipWhitespace();
+        String value = parseLabelValue();
+        if (labels.put(name, value) != null) {
+          throw new IllegalArgumentException(
+              "Duplicate Prometheus label " + name + " at line " + lineNumber);
+        }
+        skipWhitespace();
+        if (position < line.length() && line.charAt(position) == ',') {
+          position++;
+          skipWhitespace();
+          if (position < line.length() && line.charAt(position) == '}') {
+            position++;
+            return labels;
+          }
+          continue;
+        }
+        expect('}');
+        return labels;
+      }
+      throw new IllegalArgumentException("Unclosed Prometheus label set at 
line " + lineNumber);
+    }
+
+    private String parseLabelName() {
+      int start = position;
+      while (position < line.length() && line.charAt(position) != '=') {
+        if (line.charAt(position) == ',' || line.charAt(position) == '}') {
+          break;
+        }
+        position++;
+      }
+      String name = line.substring(start, position).trim();
+      if (name.isEmpty()) {
+        throw new IllegalArgumentException("Illegal Prometheus label name at 
line " + lineNumber);
+      }
+      return name;
+    }
+
+    private String parseLabelValue() {
+      expect('"');
+      StringBuilder builder = new StringBuilder();
+      while (position < line.length()) {
+        char c = line.charAt(position++);
+        if (c == '"') {
+          return builder.toString();
+        }
+        if (c == '\\') {
+          if (position >= line.length()) {
+            throw new IllegalArgumentException(
+                "Illegal Prometheus label escape at line " + lineNumber);
+          }
+          char escaped = line.charAt(position++);
+          switch (escaped) {
+            case 'n':
+              builder.append('\n');
+              break;
+            case '\\':
+            case '"':
+              builder.append(escaped);
+              break;
+            default:
+              builder.append(escaped);
+              break;
+          }
+        } else {
+          builder.append(c);
+        }
+      }
+      throw new IllegalArgumentException("Unclosed Prometheus label value at 
line " + lineNumber);
+    }
+
+    private double parseValue() {
+      String token = parseToken("sample value");
+      return parsePrometheusDouble(token, lineNumber);
+    }
+
+    private long parseOptionalTimestamp(long defaultTimestamp) {
+      if (position >= line.length()) {
+        return defaultTimestamp;
+      }
+      String token = parseToken("sample timestamp");
+      try {
+        return convertTimestamp(Long.parseLong(token), lineNumber);
+      } catch (NumberFormatException e) {
+        throw new IllegalArgumentException(
+            "Illegal Prometheus sample timestamp " + token + " at line " + 
lineNumber, e);
+      }
+    }
+
+    private String parseToken(String name) {
+      if (position >= line.length() || 
Character.isWhitespace(line.charAt(position))) {
+        throw new IllegalArgumentException("Missing Prometheus " + name + " at 
line " + lineNumber);
+      }
+      int start = position;
+      while (position < line.length() && 
!Character.isWhitespace(line.charAt(position))) {
+        position++;
+      }
+      return line.substring(start, position);
+    }
+
+    private void skipWhitespace() {
+      while (position < line.length() && 
Character.isWhitespace(line.charAt(position))) {
+        position++;
+      }
+    }
+
+    private void expect(char expected) {
+      if (position >= line.length() || line.charAt(position) != expected) {
+        throw new IllegalArgumentException(
+            "Expected '" + expected + "' in Prometheus sample at line " + 
lineNumber);
+      }
+      position++;
+    }
+
+    private void expectEnd() {
+      if (position != line.length()) {
+        throw new IllegalArgumentException(
+            "Unexpected Prometheus sample content at line " + lineNumber);
+      }
+    }
+  }
+}
diff --git 
a/external-service-impl/metric-scrape/src/test/java/org/apache/iotdb/metricscrape/PrometheusTextParserTest.java
 
b/external-service-impl/metric-scrape/src/test/java/org/apache/iotdb/metricscrape/PrometheusTextParserTest.java
new file mode 100644
index 00000000000..15f36474bac
--- /dev/null
+++ 
b/external-service-impl/metric-scrape/src/test/java/org/apache/iotdb/metricscrape/PrometheusTextParserTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.iotdb.metricscrape;
+
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class PrometheusTextParserTest {
+
+  @Test
+  public void testParseSamples() {
+    String text =
+        "# HELP up Whether the target is up.\n"
+            + "# TYPE up gauge\n"
+            + "up{job=\"iotdb\",instance=\"127.0.0.1:6667\"} 1 1635232143960\n"
+            + "rpc:latency_seconds{method=\"query\",escaped=\"a\\\\b\\\"c\",} 
+Inf\n";
+
+    List<PrometheusSample> samples = new PrometheusTextParser().parse(text, 
100);
+
+    assertEquals(2, samples.size());
+    PrometheusSample first = samples.get(0);
+    assertEquals("up", first.getMetricName());
+    assertEquals("iotdb", first.getLabels().get("job"));
+    assertEquals("127.0.0.1:6667", first.getLabels().get("instance"));
+    assertEquals(1.0, first.getValue(), 0.0);
+    assertEquals(1635232143960L, first.getTimestamp());
+
+    PrometheusSample second = samples.get(1);
+    assertEquals("rpc:latency_seconds", second.getMetricName());
+    assertEquals("query", second.getLabels().get("method"));
+    assertEquals("a\\b\"c", second.getLabels().get("escaped"));
+    assertTrue(Double.isInfinite(second.getValue()));
+    assertEquals(100, second.getTimestamp());
+  }
+
+  @Test
+  public void testParseIoTDBPrometheusReporterSamples() {
+    String text =
+        "# HELP file_count\n"
+            + "# TYPE file_count gauge\n"
+            + 
"file_count{cluster=\"defaultCluster\",nodeType=\"DataNode\",nodeId=\"1\",} 
1\n";
+
+    List<PrometheusSample> samples = new PrometheusTextParser().parse(text, 
100);
+
+    assertEquals(1, samples.size());
+    PrometheusSample sample = samples.get(0);
+    assertEquals("file_count", sample.getMetricName());
+    assertEquals("defaultCluster", sample.getLabels().get("cluster"));
+    assertEquals("DataNode", sample.getLabels().get("nodeType"));
+    assertEquals("1", sample.getLabels().get("nodeId"));
+    assertEquals(1.0, sample.getValue(), 0.0);
+    assertEquals(100, sample.getTimestamp());
+  }
+
+  @Test
+  public void testParseIoTDBMetricNameAndTagKey() {
+    String text = "metric[name]{tag-key=\"value\",} 1\n";
+
+    List<PrometheusSample> samples = new PrometheusTextParser().parse(text, 
100);
+
+    assertEquals(1, samples.size());
+    assertEquals("metric[name]", samples.get(0).getMetricName());
+    assertEquals("value", samples.get(0).getLabels().get("tag-key"));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testRejectDuplicateLabel() {
+    new PrometheusTextParser().parse("up{job=\"a\",job=\"b\"} 1", 100);
+  }
+}
diff --git a/external-service-impl/pom.xml b/external-service-impl/pom.xml
index 04483b289f3..749e1efbba2 100644
--- a/external-service-impl/pom.xml
+++ b/external-service-impl/pom.xml
@@ -31,6 +31,7 @@
     <packaging>pom</packaging>
     <modules>
         <module>mqtt</module>
+        <module>metric-scrape</module>
         <module>rest</module>
         <module>rest-openapi</module>
     </modules>
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
index 9b177cffcfa..0f814c9cc90 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
@@ -125,6 +125,24 @@ public class IoTDBConfig {
   /** Max mqtt message size. Unit: byte */
   private int mqttMaxMessageSize = 1048576;
 
+  /** Whether to enable the metric scrape service. */
+  private boolean enableMetricScrapeService = false;
+
+  /** The comma separated metric scrape target URLs. */
+  private String metricScrapeTargets = "";
+
+  /** The metric scrape interval in seconds. */
+  private int metricScrapeIntervalSeconds = 15;
+
+  /** The table model database used by metric scrape service. */
+  private String metricScrapeDatabase = "metrics";
+
+  /** The table used by metric scrape service. */
+  private String metricScrapeTable = "iotdb_metrics";
+
+  /** The HTTP timeout used by metric scrape service. Unit: millisecond */
+  private int metricScrapeHttpTimeoutMs = 10000;
+
   /** Rpc binding address. */
   private String rpcAddress = "127.0.0.1";
 
@@ -2608,6 +2626,54 @@ public class IoTDBConfig {
     this.mqttMaxMessageSize = mqttMaxMessageSize;
   }
 
+  public boolean isEnableMetricScrapeService() {
+    return enableMetricScrapeService;
+  }
+
+  public void setEnableMetricScrapeService(boolean enableMetricScrapeService) {
+    this.enableMetricScrapeService = enableMetricScrapeService;
+  }
+
+  public String getMetricScrapeTargets() {
+    return metricScrapeTargets;
+  }
+
+  public void setMetricScrapeTargets(String metricScrapeTargets) {
+    this.metricScrapeTargets = metricScrapeTargets;
+  }
+
+  public int getMetricScrapeIntervalSeconds() {
+    return metricScrapeIntervalSeconds;
+  }
+
+  public void setMetricScrapeIntervalSeconds(int metricScrapeIntervalSeconds) {
+    this.metricScrapeIntervalSeconds = metricScrapeIntervalSeconds;
+  }
+
+  public String getMetricScrapeDatabase() {
+    return metricScrapeDatabase;
+  }
+
+  public void setMetricScrapeDatabase(String metricScrapeDatabase) {
+    this.metricScrapeDatabase = metricScrapeDatabase;
+  }
+
+  public String getMetricScrapeTable() {
+    return metricScrapeTable;
+  }
+
+  public void setMetricScrapeTable(String metricScrapeTable) {
+    this.metricScrapeTable = metricScrapeTable;
+  }
+
+  public int getMetricScrapeHttpTimeoutMs() {
+    return metricScrapeHttpTimeoutMs;
+  }
+
+  public void setMetricScrapeHttpTimeoutMs(int metricScrapeHttpTimeoutMs) {
+    this.metricScrapeHttpTimeoutMs = metricScrapeHttpTimeoutMs;
+  }
+
   public int getTagAttributeFlushInterval() {
     return tagAttributeFlushInterval;
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
index 7193fd27c58..23f79d1f676 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
@@ -882,6 +882,9 @@ public class IoTDBDescriptor {
     // mqtt
     loadMqttProps(properties);
 
+    // metric scrape
+    loadMetricScrapeProps(properties);
+
     conf.setIntoOperationBufferSizeInByte(
         Long.parseLong(
             properties.getProperty(
@@ -2028,6 +2031,40 @@ public class IoTDBDescriptor {
     }
   }
 
+  // metric scrape related
+  private void loadMetricScrapeProps(TrimProperties properties) {
+    if (properties.getProperty(IoTDBConstant.ENABLE_METRIC_SCRAPE) != null) {
+      conf.setEnableMetricScrapeService(
+          
Boolean.parseBoolean(properties.getProperty(IoTDBConstant.ENABLE_METRIC_SCRAPE).trim()));
+    }
+
+    if (properties.getProperty(IoTDBConstant.METRIC_SCRAPE_TARGETS) != null) {
+      conf.setMetricScrapeTargets(
+          properties.getProperty(IoTDBConstant.METRIC_SCRAPE_TARGETS).trim());
+    }
+
+    if (properties.getProperty(IoTDBConstant.METRIC_SCRAPE_INTERVAL_SECONDS) 
!= null) {
+      conf.setMetricScrapeIntervalSeconds(
+          Integer.parseInt(
+              
properties.getProperty(IoTDBConstant.METRIC_SCRAPE_INTERVAL_SECONDS).trim()));
+    }
+
+    if (properties.getProperty(IoTDBConstant.METRIC_SCRAPE_DATABASE) != null) {
+      conf.setMetricScrapeDatabase(
+          properties.getProperty(IoTDBConstant.METRIC_SCRAPE_DATABASE).trim());
+    }
+
+    if (properties.getProperty(IoTDBConstant.METRIC_SCRAPE_TABLE) != null) {
+      
conf.setMetricScrapeTable(properties.getProperty(IoTDBConstant.METRIC_SCRAPE_TABLE).trim());
+    }
+
+    if (properties.getProperty(IoTDBConstant.METRIC_SCRAPE_HTTP_TIMEOUT_MS) != 
null) {
+      conf.setMetricScrapeHttpTimeoutMs(
+          Integer.parseInt(
+              
properties.getProperty(IoTDBConstant.METRIC_SCRAPE_HTTP_TIMEOUT_MS).trim()));
+    }
+  }
+
   // timed flush memtable
   private void loadTimedService(TrimProperties properties) throws IOException {
     conf.setEnableTimedFlushSeqMemtable(
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/externalservice/BuiltinExternalServices.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/externalservice/BuiltinExternalServices.java
index 157734aef56..302a392aa41 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/externalservice/BuiltinExternalServices.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/service/externalservice/BuiltinExternalServices.java
@@ -32,7 +32,11 @@ public enum BuiltinExternalServices {
   REST(
       "REST",
       "org.apache.iotdb.rest.RestService",
-      
IoTDBRestServiceDescriptor.getInstance().getConfig()::isEnableRestService);
+      
IoTDBRestServiceDescriptor.getInstance().getConfig()::isEnableRestService),
+  METRIC_SCRAPE(
+      "METRIC_SCRAPE",
+      "org.apache.iotdb.metricscrape.MetricScrapeService",
+      IoTDBDescriptor.getInstance().getConfig()::isEnableMetricScrapeService);
 
   private final String serviceName;
   private final String className;
diff --git 
a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
 
b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
index 54d9ccaf5ce..80006bf4832 100644
--- 
a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
+++ 
b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
@@ -2141,6 +2141,40 @@ mqtt_payload_formatter=json
 # Datatype: int
 mqtt_max_message_size=1048576
 
+####################
+### Metric Scrape Configuration
+####################
+
+# whether to enable the metric scrape service.
+# effectiveMode: restart
+# Datatype: boolean
+enable_metric_scrape_service=false
+
+# the comma separated Prometheus text exposition target URLs to scrape.
+# effectiveMode: restart
+# Datatype: String
+metric_scrape_targets=
+
+# the interval between two scrapes of the same target in seconds.
+# effectiveMode: restart
+# Datatype: int
+metric_scrape_interval_seconds=15
+
+# the table model database used by the metric scrape service.
+# effectiveMode: restart
+# Datatype: String
+metric_scrape_database=metrics
+
+# the table used by the metric scrape service.
+# effectiveMode: restart
+# Datatype: String
+metric_scrape_table=iotdb_metrics
+
+# the HTTP connect and read timeout used by the metric scrape service in 
milliseconds.
+# effectiveMode: restart
+# Datatype: int
+metric_scrape_http_timeout_ms=10000
+
 ####################
 ### IoTDB-AI Configuration
 ####################
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/IoTDBConstant.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/IoTDBConstant.java
index 03e9a53db60..5d9332cff98 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/IoTDBConstant.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/IoTDBConstant.java
@@ -290,6 +290,14 @@ public class IoTDBConstant {
   public static final String MQTT_DATA_PATH = "mqtt_data_path";
   public static final String MQTT_MAX_MESSAGE_SIZE = "mqtt_max_message_size";
 
+  // metric scrape
+  public static final String ENABLE_METRIC_SCRAPE = 
"enable_metric_scrape_service";
+  public static final String METRIC_SCRAPE_TARGETS = "metric_scrape_targets";
+  public static final String METRIC_SCRAPE_INTERVAL_SECONDS = 
"metric_scrape_interval_seconds";
+  public static final String METRIC_SCRAPE_DATABASE = "metric_scrape_database";
+  public static final String METRIC_SCRAPE_TABLE = "metric_scrape_table";
+  public static final String METRIC_SCRAPE_HTTP_TIMEOUT_MS = 
"metric_scrape_http_timeout_ms";
+
   // thrift
   public static final int DEFAULT_FETCH_SIZE = 5000;
   public static final int DEFAULT_CONNECTION_TIMEOUT_MS = 0;

Reply via email to