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

yuluo-yx 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 0734013a2a fix: avoid LLM generates sql has sql injection error (#4200)
0734013a2a is described below

commit 0734013a2ac157a5df2ccd800bbb5dae201cd7c3
Author: shown <[email protected]>
AuthorDate: Sun Jul 12 22:45:19 2026 +0800

    fix: avoid LLM generates sql has sql injection error (#4200)
    
    Signed-off-by: yuluo-yx <[email protected]>
---
 mcp-servers/mcp-log-server/README.md               |  63 ++++++-
 .../mcp/log/server/service/LogService.java         | 183 ++++++++++++++-----
 .../mcp/server/service/LogServiceTest.java         | 201 +++++++++++++++++++++
 3 files changed, 398 insertions(+), 49 deletions(-)

diff --git a/mcp-servers/mcp-log-server/README.md 
b/mcp-servers/mcp-log-server/README.md
index 89bac97f12..b353142df1 100644
--- a/mcp-servers/mcp-log-server/README.md
+++ b/mcp-servers/mcp-log-server/README.md
@@ -1,12 +1,50 @@
-# HertzBeat MCP
+# HertzBeat 日志 MCP
 
-## HertzBeat Log MCP
+该 MCP 服务基于 GreptimeDB 查询 HertzBeat 自身运行日志。使用前需要启用 GreptimeDB 日志写入。
 
-Log MCP Service Based on GreptimeDB.
+## 结构化只读查询
 
-- GreptimeDB log writing needs to be enabled.
+服务只暴露 `query_logs`,不再接收或执行调用方提供的 SQL。服务端生成的查询固定为:
 
-## Claude Desktop Integration (stdio)
+```sql
+SELECT timestamp, severity_text, body
+FROM hzb_logs
+WHERE <结构化过滤条件>
+ORDER BY timestamp DESC
+LIMIT <1-100>
+```
+
+`query_logs` 支持以下可选参数:
+
+| 参数 | 类型 | 说明 |
+| --- | --- | --- |
+| `severity` | 字符串 | `TRACE`、`DEBUG`、`INFO`、`WARN`、`ERROR` 或 `FATAL` |
+| `keyword` | 字符串 | 日志正文关键词,最长 256 个字符 |
+| `startTime` | 整数 | 开始时间,Unix 毫秒时间戳 |
+| `endTime` | 整数 | 结束时间,Unix 毫秒时间戳 |
+| `limit` | 整数 | 返回条数,默认为 20,范围为 1~100 |
+
+调用示例:
+
+```json
+{
+  "severity": "ERROR",
+  "keyword": "connection refused",
+  "startTime": 1783785600000,
+  "endTime": 1783872000000,
+  "limit": 20
+}
+```
+
+当前 `hzb_logs` 表没有 `monitorId` 字段,因此本接口不提供无效的监控 ID 过滤。如果后续需要该能力,应先在 
OpenTelemetry 日志写入链中定义并提取统一的监控 ID 字段。
+
+## GreptimeDB 账号
+
+服务支持通过 `greptime.username` 和 `greptime.password` 发送 HTTP Basic 
Authentication。用户名和密码必须同时配置;建议配合 HTTPS 或可信内网使用。
+
+项目当前 Docker Compose 使用 GreptimeDB 
`v0.14.3`,该版本只提供身份认证,不能限制用户为只读权限。因此当前真正生效的安全边界是“删除原始 SQL参数并固定生成单条 `SELECT`”。使用 
GreptimeDB 1.0 及以上版本时,应为该 MCP 配置独立的 `ro`/`readonly` 账号。
+
+## Claude Desktop 集成(stdio)
 
 ```json
 {
@@ -17,11 +55,20 @@ Log MCP Service Based on GreptimeDB.
                 "-Dspring.ai.mcp.server.stdio=true",
                 "-Dspring.main.web-application-type=none",
                 "-Dlogging.pattern.console=",
-                "-Dgreptime.url=http://${IP}:4000";,
                 "-jar",
                 "${PATH}/hertzbeat-mcp-2.0-SNAPSHOT.jar"
-            ]
+            ],
+            "env": {
+                "GREPTIME_URL": "http://${IP}:4000";,
+                "GREPTIME_DATABASE": "public",
+                "GREPTIME_USERNAME": "${READ_ONLY_USERNAME}",
+                "GREPTIME_PASSWORD": "${READ_ONLY_PASSWORD}"
+            }
         }
     }
 }
-```
\ No newline at end of file
+```
+
+不启用 GreptimeDB 认证时,可以省略 `GREPTIME_USERNAME` 和 `GREPTIME_PASSWORD`;不能只配置其中一个。
+
+旧的 `getHertzbeatLog({"querySql": "..."})` Tool 已直接移除,不保留兼容入口,以免继续暴露任意 SQL执行能力。
diff --git 
a/mcp-servers/mcp-log-server/src/main/java/org/apache/hertzbeat/mcp/log/server/service/LogService.java
 
b/mcp-servers/mcp-log-server/src/main/java/org/apache/hertzbeat/mcp/log/server/service/LogService.java
index c5023dc106..d87154561f 100644
--- 
a/mcp-servers/mcp-log-server/src/main/java/org/apache/hertzbeat/mcp/log/server/service/LogService.java
+++ 
b/mcp-servers/mcp-log-server/src/main/java/org/apache/hertzbeat/mcp/log/server/service/LogService.java
@@ -22,7 +22,9 @@ import com.jayway.jsonpath.ReadContext;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.ai.tool.annotation.Tool;
 import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.stereotype.Service;
 import org.springframework.util.LinkedMultiValueMap;
@@ -33,79 +35,178 @@ import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 
 /**
- * Log query service
+ * Log query service.
  */
 @Service
 @Slf4j
 public class LogService {
 
+    private static final String BASE_QUERY = "SELECT timestamp, severity_text, 
body FROM hzb_logs";
     private static final String TIMESTAMP_COLUMN = "timestamp";
     private static final String SEVERITY_TEXT_COLUMN = "severity_text";
     private static final String BODY_COLUMN = "body";
+    private static final int DEFAULT_LIMIT = 20;
+    private static final int MAX_LIMIT = 100;
+    private static final int MAX_KEYWORD_LENGTH = 256;
+    private static final long NANOS_PER_MILLISECOND = 1_000_000L;
+    private static final Set<String> SUPPORTED_SEVERITIES =
+            Set.of("TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL");
     private static final DateTimeFormatter DATE_FORMATTER = 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
-    
+
     private final RestClient restClient;
+    private final String database;
 
-    public LogService(@Value("${greptime.url}") String greptimeUrl) {
-        this.restClient = RestClient.builder()
-                .baseUrl(greptimeUrl)
-                .defaultHeader("Accept", "application/json")
-                .defaultHeader("Content-Type", 
"application/x-www-form-urlencoded")
-                .build();
+    /**
+     * Creates a log query service.
+     *
+     * @param greptimeUrl GreptimeDB URL
+     * @param database GreptimeDB database name
+     * @param username GreptimeDB username
+     * @param password GreptimeDB password
+     */
+    @Autowired
+    public LogService(@Value("${greptime.url}") String greptimeUrl,
+                      @Value("${greptime.database:public}") String database,
+                      @Value("${greptime.username:}") String username,
+                      @Value("${greptime.password:}") String password) {
+        this(RestClient.builder(), greptimeUrl, database, username, password);
     }
 
-    @Tool(description = "System log query tool that supports filtering by 
time, log level, and content")
-    public String getHertzbeatLog(
-            @ToolParam(description = """
-                Query system logs with support for filtering by time, log 
level, and content.
-                
-                Usage:
-                1. Table name: hzb_log
-                2. Common query examples:
-                   - Get latest 10 logs: SELECT * FROM hzb_log ORDER BY 
timestamp DESC LIMIT 10
-                   - Query ERROR level logs: SELECT * FROM hzb_log WHERE 
severity_number=17
-                   - Query specific time range: SELECT * FROM hzb_log WHERE 
timestamp > '2024-01-01 00:00:00'
-                   - Query last ten minutes: SELECT * FROM hzb_log WHERE  
timestamp >= now() - Interval '10m' AND timestamp < now()
-                   - Query logs at or above the INFO level: SELECT * FROM 
hzb_log WHERE severity_number >= 9
-                
-                Field descriptions:
-                1. severity_number (log level):
-                   - 5: DEBUG
-                   - 9: INFO
-                   - 13: WARN
-                   - 17: ERROR
-                2. timestamp: log timestamp
-                3. body: log content
-                """) String querySql) {
-        
-        if (!isValidQuery(querySql)) {
-            return "Invalid query statement";
+    LogService(RestClient.Builder restClientBuilder, String greptimeUrl, 
String database,
+               String username, String password) {
+        boolean hasUsername = username != null && !username.isBlank();
+        boolean hasPassword = password != null && !password.isBlank();
+        if (hasUsername != hasPassword) {
+            throw new IllegalArgumentException("GreptimeDB username and 
password must be configured together");
+        }
+
+        RestClient.Builder builder = restClientBuilder
+                .baseUrl(greptimeUrl)
+                .defaultHeader(HttpHeaders.ACCEPT, 
MediaType.APPLICATION_JSON_VALUE)
+                .defaultHeader(HttpHeaders.CONTENT_TYPE, 
MediaType.APPLICATION_FORM_URLENCODED_VALUE);
+        if (hasUsername) {
+            builder.defaultHeaders(headers -> headers.setBasicAuth(username, 
password));
         }
+        this.restClient = builder.build();
+        this.database = database == null || database.isBlank() ? "public" : 
database;
+    }
 
+    /**
+     * Queries HertzBeat system logs using structured filters.
+     *
+     * @param severity log severity
+     * @param keyword log body keyword
+     * @param startTime start time as a Unix timestamp in milliseconds
+     * @param endTime end time as a Unix timestamp in milliseconds
+     * @param limit maximum number of records to return
+     * @return formatted log query results
+     */
+    @Tool(name = "query_logs", description = "Query HertzBeat system logs with 
structured read-only filters")
+    public String queryLogs(
+            @ToolParam(required = false,
+                    description = "Log severity; supported values: TRACE, 
DEBUG, INFO, WARN, ERROR, FATAL")
+            String severity,
+            @ToolParam(required = false, description = "Keyword to search in 
the log body; maximum 256 characters")
+            String keyword,
+            @ToolParam(required = false, description = "Start time as a Unix 
timestamp in milliseconds")
+            Long startTime,
+            @ToolParam(required = false, description = "End time as a Unix 
timestamp in milliseconds")
+            Long endTime,
+            @ToolParam(required = false,
+                    description = "Maximum number of records to return; 
defaults to 20 and cannot exceed 100")
+            Integer limit) {
         try {
-            String response = executeQuery(querySql);
+            String response = executeQuery(buildQuery(severity, keyword, 
startTime, endTime, limit));
             return formatQueryResults(response);
+        } catch (IllegalArgumentException e) {
+            return "Invalid query parameters: " + e.getMessage();
         } catch (Exception e) {
             log.error("Failed to query logs", e);
-            return "Failed to query logs: " + e.getMessage();
+            return "Failed to query logs";
+        }
+    }
+
+    static String buildQuery(String severity, String keyword, Long startTime, 
Long endTime, Integer limit) {
+        if (startTime != null && startTime < 0) {
+            throw new IllegalArgumentException("Start time must not be 
negative");
+        }
+        if (endTime != null && endTime < 0) {
+            throw new IllegalArgumentException("End time must not be 
negative");
+        }
+        if (startTime != null && endTime != null && startTime > endTime) {
+            throw new IllegalArgumentException("Start time must not be later 
than end time");
+        }
+
+        int queryLimit = limit == null ? DEFAULT_LIMIT : limit;
+        if (queryLimit < 1 || queryLimit > MAX_LIMIT) {
+            throw new IllegalArgumentException("Limit must be between 1 and 
100");
+        }
+
+        List<String> conditions = new ArrayList<>(4);
+        if (startTime != null) {
+            conditions.add("timestamp >= " + toNanoseconds(startTime, "Start 
time"));
+        }
+        if (endTime != null) {
+            conditions.add("timestamp <= " + toNanoseconds(endTime, "End 
time"));
+        }
+
+        String normalizedSeverity = normalizeSeverity(severity);
+        if (normalizedSeverity != null) {
+            conditions.add("severity_text = '" + normalizedSeverity + "'");
+        }
+
+        if (keyword != null && !keyword.isBlank()) {
+            String normalizedKeyword = keyword.strip();
+            if (normalizedKeyword.length() > MAX_KEYWORD_LENGTH) {
+                throw new IllegalArgumentException("Log keyword must not 
exceed 256 characters");
+            }
+            conditions.add("matches_term(body, '" + 
escapeSqlLiteral(normalizedKeyword) + "')");
+        }
+
+        StringBuilder query = new StringBuilder(BASE_QUERY);
+        if (!conditions.isEmpty()) {
+            query.append(" WHERE ").append(String.join(" AND ", conditions));
+        }
+        return query.append(" ORDER BY timestamp DESC LIMIT 
").append(queryLimit).toString();
+    }
+
+    private static String normalizeSeverity(String severity) {
+        if (severity == null || severity.isBlank()) {
+            return null;
+        }
+        String normalizedSeverity = severity.strip().toUpperCase(Locale.ROOT);
+        if (!SUPPORTED_SEVERITIES.contains(normalizedSeverity)) {
+            throw new IllegalArgumentException("Unsupported log severity");
+        }
+        return normalizedSeverity;
+    }
+
+    private static long toNanoseconds(long epochMilliseconds, String 
fieldName) {
+        try {
+            return Math.multiplyExact(epochMilliseconds, 
NANOS_PER_MILLISECOND);
+        } catch (ArithmeticException e) {
+            throw new IllegalArgumentException(fieldName + " is out of the 
supported range", e);
         }
     }
 
-    private boolean isValidQuery(String sql) {
-        return sql != null && sql.toLowerCase().contains("hzb_log");
+    private static String escapeSqlLiteral(String value) {
+        return value.replace("'", "''");
     }
 
     private String executeQuery(String sql) {
         MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
         formData.add("sql", sql);
-        log.debug("Executing SQL query: {}", sql);
-        
+        log.debug("Executing structured log query");
+
         return restClient.post()
-                .uri("/v1/sql?db=public")
+                .uri(uriBuilder -> uriBuilder.path("/v1/sql").queryParam("db", 
database).build())
                 .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                 .body(formData)
                 .retrieve()
diff --git 
a/mcp-servers/mcp-log-server/src/test/java/org/apache/hertzbeat/mcp/server/service/LogServiceTest.java
 
b/mcp-servers/mcp-log-server/src/test/java/org/apache/hertzbeat/mcp/server/service/LogServiceTest.java
new file mode 100644
index 0000000000..9608222888
--- /dev/null
+++ 
b/mcp-servers/mcp-log-server/src/test/java/org/apache/hertzbeat/mcp/server/service/LogServiceTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.mcp.server.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static 
org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.content;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.header;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.method;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+import static 
org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;
+import static 
org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.method.MethodToolCallbackProvider;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestClient;
+import org.springframework.test.web.client.MockRestServiceServer;
+
+/**
+ * Security tests for {@link LogService} queries.
+ */
+class LogServiceTest {
+
+    private static final String BASE_QUERY = "SELECT timestamp, severity_text, 
body FROM hzb_logs";
+
+    @Test
+    void shouldBuildDefaultReadOnlyQuery() {
+        assertThat(LogService.buildQuery(null, null, null, null, null))
+                .isEqualTo(BASE_QUERY + " ORDER BY timestamp DESC LIMIT 20");
+    }
+
+    @Test
+    void shouldExposeOnlyStructuredToolParameters() {
+        LogService service = new LogService(
+                RestClient.builder(), "http://localhost:4000";, "public", "", 
"");
+        ToolCallback[] callbacks = 
MethodToolCallbackProvider.builder().toolObjects(service).build().getToolCallbacks();
+
+        assertThat(callbacks).singleElement().satisfies(callback -> {
+            
assertThat(callback.getToolDefinition().name()).isEqualTo("query_logs");
+            assertThat(callback.getToolDefinition().inputSchema())
+                    .contains("severity", "keyword", "startTime", "endTime", 
"limit")
+                    .doesNotContain("querySql");
+        });
+    }
+
+    @Test
+    void shouldBuildQueryFromValidatedFilters() {
+        String query = LogService.buildQuery(
+                " error ", " x'); DELETE FROM hzb_logs; -- ", 1L, 2L, 10);
+
+        assertThat(query).isEqualTo(BASE_QUERY
+                + " WHERE timestamp >= 1000000"
+                + " AND timestamp <= 2000000"
+                + " AND severity_text = 'ERROR'"
+                + " AND matches_term(body, 'x''); DELETE FROM hzb_logs; --')"
+                + " ORDER BY timestamp DESC LIMIT 10");
+    }
+
+    @Test
+    void shouldRejectInvalidFilters() {
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> LogService.buildQuery("NOTICE", null, null, 
null, null));
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> LogService.buildQuery(null, null, -1L, null, 
null));
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> LogService.buildQuery(null, null, null, -1L, 
null));
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> LogService.buildQuery(null, null, 2L, 1L, 
null));
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> LogService.buildQuery(null, null, 
Long.MAX_VALUE, null, null));
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> LogService.buildQuery(null, "x".repeat(257), 
null, null, null));
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> LogService.buildQuery(null, null, null, 
null, 0));
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> LogService.buildQuery(null, null, null, 
null, 101));
+    }
+
+    @Test
+    void shouldSendGeneratedSelectWithBasicAuthentication() {
+        RestClient.Builder restClientBuilder = RestClient.builder();
+        MockRestServiceServer server = 
MockRestServiceServer.bindTo(restClientBuilder).build();
+        LogService service = new LogService(
+                restClientBuilder, "http://localhost:4000";, "observability", 
"reader", "secret");
+        String query = BASE_QUERY + " ORDER BY timestamp DESC LIMIT 20";
+        MultiValueMap<String, String> expectedForm = new 
LinkedMultiValueMap<>();
+        expectedForm.setAll(Map.of("sql", query));
+        String credentials = Base64.getEncoder()
+                
.encodeToString("reader:secret".getBytes(StandardCharsets.UTF_8));
+
+        
server.expect(requestTo("http://localhost:4000/v1/sql?db=observability";))
+                .andExpect(method(HttpMethod.POST))
+                .andExpect(header(HttpHeaders.AUTHORIZATION, "Basic " + 
credentials))
+                .andExpect(content().formData(expectedForm))
+                .andRespond(withSuccess(emptyResult(), 
MediaType.APPLICATION_JSON));
+
+        assertThat(service.queryLogs(null, null, null, null, 
null)).contains("No data");
+        server.verify();
+    }
+
+    @Test
+    void shouldFormatReturnedLogs() {
+        RestClient.Builder restClientBuilder = RestClient.builder();
+        MockRestServiceServer server = 
MockRestServiceServer.bindTo(restClientBuilder).build();
+        LogService service = new LogService(restClientBuilder, 
"http://localhost:4000";, "public", "", "");
+        server.expect(requestTo("http://localhost:4000/v1/sql?db=public";))
+                .andRespond(withSuccess(resultWithOneLog(), 
MediaType.APPLICATION_JSON));
+
+        assertThat(service.queryLogs("ERROR", "failure", null, null, 1))
+                .contains("ERROR", "failure", "Total 1 records");
+        server.verify();
+    }
+
+    @Test
+    void shouldReturnSafeErrorMessages() {
+        RestClient.Builder restClientBuilder = RestClient.builder();
+        MockRestServiceServer server = 
MockRestServiceServer.bindTo(restClientBuilder).build();
+        LogService service = new LogService(restClientBuilder, 
"http://localhost:4000";, "public", "", "");
+
+        assertThat(service.queryLogs("NOTICE", null, null, null, null))
+                .isEqualTo("Invalid query parameters: Unsupported log 
severity");
+
+        server.expect(requestTo("http://localhost:4000/v1/sql?db=public";))
+                .andRespond(withServerError());
+        assertThat(service.queryLogs(null, null, null, null, 
null)).isEqualTo("Failed to query logs");
+        server.verify();
+    }
+
+    @Test
+    void shouldRequireCompleteCredentials() {
+        assertThatIllegalArgumentException().isThrownBy(() -> new LogService(
+                RestClient.builder(), "http://localhost:4000";, "public", 
"reader", ""));
+        assertThatIllegalArgumentException().isThrownBy(() -> new LogService(
+                RestClient.builder(), "http://localhost:4000";, "public", "", 
"secret"));
+    }
+
+    @Test
+    void shouldCreateWithProductionConstructor() {
+        assertThat(new LogService("http://localhost:4000";, "public", "", 
"")).isNotNull();
+    }
+
+    private static String emptyResult() {
+        return """
+                {
+                  "output": [{
+                    "records": {
+                      "schema": {"column_schemas": [
+                        {"name": "timestamp"},
+                        {"name": "severity_text"},
+                        {"name": "body"}
+                      ]},
+                      "rows": [],
+                      "total_rows": 0
+                    }
+                  }]
+                }
+                """;
+    }
+
+    private static String resultWithOneLog() {
+        return """
+                {
+                  "output": [{
+                    "records": {
+                      "schema": {"column_schemas": [
+                        {"name": "timestamp"},
+                        {"name": "severity_text"},
+                        {"name": "body"}
+                      ]},
+                      "rows": [[0, "ERROR", "failure"]],
+                      "total_rows": 1
+                    }
+                  }]
+                }
+                """;
+    }
+}


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

Reply via email to