Skip to content

Commit 81ad7d5

Browse files
[Feature] Add log mcp for java (apache#3254)
1 parent 9fa5b17 commit 81ad7d5

File tree

6 files changed

+403
-0
lines changed

6 files changed

+403
-0
lines changed

hertzbeat-mcp/README.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Hertzbeat-MCP
2+
3+
## Hertzbeat-Log-MCP
4+
5+
Log MCP Service Based on GreptimeDB.
6+
7+
- GreptimeDB log writing needs to be enabled.
8+
9+
## Claude Desktop Integration (stdio)
10+
11+
```json
12+
{
13+
"mcpServers": {
14+
"hertzbeat-mcp": {
15+
"command": "java",
16+
"args": [
17+
"-Dspring.ai.mcp.server.stdio=true",
18+
"-Dspring.main.web-application-type=none",
19+
"-Dlogging.pattern.console=",
20+
"-Dgreptime.url=http://${IP}:4000",
21+
"-jar",
22+
"${PATH}/hertzbeat-mcp-2.0-SNAPSHOT.jar"
23+
]
24+
}
25+
}
26+
}
27+
```

hertzbeat-mcp/pom.xml

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Licensed to the Apache Software Foundation (ASF) under one or more
4+
~ contributor license agreements. See the NOTICE file distributed with
5+
~ this work for additional information regarding copyright ownership.
6+
~ The ASF licenses this file to You under the Apache License, Version 2.0
7+
~ (the "License"); you may not use this file except in compliance with
8+
~ the License. You may obtain a copy of the License at
9+
~
10+
~ http://www.apache.org/licenses/LICENSE-2.0
11+
~
12+
~ Unless required by applicable law or agreed to in writing, software
13+
~ distributed under the License is distributed on an "AS IS" BASIS,
14+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
~ See the License for the specific language governing permissions and
16+
~ limitations under the License.
17+
-->
18+
<project xmlns="http://maven.apache.org/POM/4.0.0"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
21+
<modelVersion>4.0.0</modelVersion>
22+
<parent>
23+
<groupId>org.apache.hertzbeat</groupId>
24+
<artifactId>hertzbeat</artifactId>
25+
<version>2.0-SNAPSHOT</version>
26+
</parent>
27+
28+
<artifactId>hertzbeat-mcp</artifactId>
29+
30+
<properties>
31+
<maven.compiler.source>17</maven.compiler.source>
32+
<maven.compiler.target>17</maven.compiler.target>
33+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
34+
<spring-ai.version>1.0.0-M6</spring-ai.version>
35+
</properties>
36+
37+
<dependencyManagement>
38+
<dependencies>
39+
<dependency>
40+
<groupId>org.springframework.boot</groupId>
41+
<artifactId>spring-boot-dependencies</artifactId>
42+
<version>3.4.2</version>
43+
<type>pom</type>
44+
<scope>import</scope>
45+
</dependency>
46+
<dependency>
47+
<groupId>org.springframework.ai</groupId>
48+
<artifactId>spring-ai-bom</artifactId>
49+
<version>${spring-ai.version}</version>
50+
<type>pom</type>
51+
<scope>import</scope>
52+
</dependency>
53+
</dependencies>
54+
</dependencyManagement>
55+
56+
<dependencies>
57+
<dependency>
58+
<groupId>org.springframework.ai</groupId>
59+
<artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
60+
<version>${spring-ai.version}</version>
61+
</dependency>
62+
<!-- json path parser-->
63+
<dependency>
64+
<groupId>com.jayway.jsonpath</groupId>
65+
<artifactId>json-path</artifactId>
66+
</dependency>
67+
<dependency>
68+
<groupId>org.springframework</groupId>
69+
<artifactId>spring-web</artifactId>
70+
</dependency>
71+
</dependencies>
72+
73+
<build>
74+
<plugins>
75+
<plugin>
76+
<groupId>org.springframework.boot</groupId>
77+
<artifactId>spring-boot-maven-plugin</artifactId>
78+
<executions>
79+
<execution>
80+
<goals>
81+
<goal>repackage</goal>
82+
</goals>
83+
</execution>
84+
</executions>
85+
</plugin>
86+
<plugin>
87+
<groupId>org.apache.maven.plugins</groupId>
88+
<artifactId>maven-compiler-plugin</artifactId>
89+
<configuration>
90+
<source>17</source>
91+
<target>17</target>
92+
</configuration>
93+
</plugin>
94+
<plugin>
95+
<groupId>org.apache.maven.plugins</groupId>
96+
<artifactId>maven-compiler-plugin</artifactId>
97+
<version>${maven-compiler-plugin.version}</version>
98+
<configuration>
99+
<release>${java.version}</release>
100+
<compilerArgs>
101+
<compilerArg>-parameters</compilerArg>
102+
</compilerArgs>
103+
</configuration>
104+
</plugin>
105+
</plugins>
106+
</build>
107+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hertzbeat.mcp.server;
19+
20+
import org.apache.hertzbeat.mcp.server.service.LogService;
21+
import org.springframework.ai.tool.ToolCallbackProvider;
22+
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
23+
import org.springframework.boot.SpringApplication;
24+
import org.springframework.boot.autoconfigure.SpringBootApplication;
25+
import org.springframework.context.annotation.Bean;
26+
27+
/**
28+
* MCP Server Application
29+
*/
30+
@SpringBootApplication
31+
public class McpServerApplication {
32+
33+
public static void main(String[] args) {
34+
SpringApplication.run(McpServerApplication.class, args);
35+
}
36+
37+
@Bean
38+
public ToolCallbackProvider tools(
39+
LogService logService) {
40+
return MethodToolCallbackProvider.builder()
41+
.toolObjects(logService)
42+
.build();
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hertzbeat.mcp.server.service;
19+
20+
import com.jayway.jsonpath.JsonPath;
21+
import com.jayway.jsonpath.ReadContext;
22+
import lombok.extern.slf4j.Slf4j;
23+
import org.springframework.ai.tool.annotation.Tool;
24+
import org.springframework.ai.tool.annotation.ToolParam;
25+
import org.springframework.beans.factory.annotation.Value;
26+
import org.springframework.http.MediaType;
27+
import org.springframework.stereotype.Service;
28+
import org.springframework.util.LinkedMultiValueMap;
29+
import org.springframework.util.MultiValueMap;
30+
import org.springframework.web.client.RestClient;
31+
32+
import java.time.Instant;
33+
import java.time.LocalDateTime;
34+
import java.time.ZoneId;
35+
import java.time.format.DateTimeFormatter;
36+
import java.util.List;
37+
import java.util.Map;
38+
39+
/**
40+
* Log query service
41+
*/
42+
@Service
43+
@Slf4j
44+
public class LogService {
45+
46+
private static final String TIMESTAMP_COLUMN = "timestamp";
47+
private static final String SEVERITY_TEXT_COLUMN = "severity_text";
48+
private static final String BODY_COLUMN = "body";
49+
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
50+
51+
private final RestClient restClient;
52+
53+
public LogService(@Value("${greptime.url}") String greptimeUrl) {
54+
this.restClient = RestClient.builder()
55+
.baseUrl(greptimeUrl)
56+
.defaultHeader("Accept", "application/json")
57+
.defaultHeader("Content-Type", "application/x-www-form-urlencoded")
58+
.build();
59+
}
60+
61+
@Tool(description = "System log query tool that supports filtering by time, log level, and content")
62+
public String getHertzbeatLog(
63+
@ToolParam(description = """
64+
Query system logs with support for filtering by time, log level, and content.
65+
66+
Usage:
67+
1. Table name: hzb_log
68+
2. Common query examples:
69+
- Get latest 10 logs: SELECT * FROM hzb_log ORDER BY timestamp DESC LIMIT 10
70+
- Query ERROR level logs: SELECT * FROM hzb_log WHERE severity_number=17
71+
- Query specific time range: SELECT * FROM hzb_log WHERE timestamp > '2024-01-01 00:00:00'
72+
73+
Field descriptions:
74+
1. severity_number (log level):
75+
- 5: DEBUG
76+
- 9: INFO
77+
- 13: WARN
78+
- 17: ERROR
79+
2. timestamp: log timestamp
80+
3. body: log content
81+
""") String querySql) {
82+
83+
if (!isValidQuery(querySql)) {
84+
return "Invalid query statement";
85+
}
86+
87+
try {
88+
String response = executeQuery(querySql);
89+
return formatQueryResults(response);
90+
} catch (Exception e) {
91+
log.error("Failed to query logs", e);
92+
return "Failed to query logs: " + e.getMessage();
93+
}
94+
}
95+
96+
private boolean isValidQuery(String sql) {
97+
return sql != null && sql.toLowerCase().contains("hzb_log");
98+
}
99+
100+
private String executeQuery(String sql) {
101+
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
102+
formData.add("sql", sql);
103+
log.debug("Executing SQL query: {}", sql);
104+
105+
return restClient.post()
106+
.uri("/v1/sql?db=public")
107+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
108+
.body(formData)
109+
.retrieve()
110+
.body(String.class);
111+
}
112+
113+
private String formatQueryResults(String response) {
114+
ReadContext ctx = JsonPath.parse(response);
115+
List<Map<String, Object>> columnSchemas = ctx.read("$.output[0].records.schema.column_schemas");
116+
List<List<Object>> rows = ctx.read("$.output[0].records.rows");
117+
int totalRows = ctx.read("$.output[0].records.total_rows");
118+
119+
ColumnIndices indices = findColumnIndices(columnSchemas);
120+
StringBuilder result = new StringBuilder()
121+
.append("Query Results:\n\n")
122+
.append("Log Time\t\t\tLog Level\tLog Content\n")
123+
.append("----------------------------------------------------\n");
124+
125+
if (rows != null && !rows.isEmpty()) {
126+
formatRows(rows, indices, result);
127+
result.append("\nTotal ").append(totalRows).append(" records");
128+
} else {
129+
result.append("No data");
130+
}
131+
132+
return result.toString();
133+
}
134+
135+
private record ColumnIndices(int timestamp, int severityText, int body) {}
136+
137+
private ColumnIndices findColumnIndices(List<Map<String, Object>> columnSchemas) {
138+
int timestampIndex = -1;
139+
int severityTextIndex = -1;
140+
int bodyIndex = -1;
141+
142+
for (int i = 0; i < columnSchemas.size(); i++) {
143+
String columnName = (String) columnSchemas.get(i).get("name");
144+
switch (columnName) {
145+
case TIMESTAMP_COLUMN -> timestampIndex = i;
146+
case SEVERITY_TEXT_COLUMN -> severityTextIndex = i;
147+
case BODY_COLUMN -> bodyIndex = i;
148+
default -> {
149+
// Ignore other columns
150+
}
151+
}
152+
}
153+
154+
return new ColumnIndices(timestampIndex, severityTextIndex, bodyIndex);
155+
}
156+
157+
private void formatRows(List<List<Object>> rows, ColumnIndices indices, StringBuilder result) {
158+
for (List<Object> row : rows) {
159+
appendTimestamp(row, indices.timestamp(), result);
160+
appendSeverity(row, indices.severityText(), result);
161+
appendBody(row, indices.body(), result);
162+
result.append("\n");
163+
}
164+
}
165+
166+
private void appendTimestamp(List<Object> row, int index, StringBuilder result) {
167+
if (index >= 0 && index < row.size()) {
168+
Object value = row.get(index);
169+
if (value instanceof Number) {
170+
long timestamp = ((Number) value).longValue();
171+
LocalDateTime dateTime = LocalDateTime.ofInstant(
172+
Instant.ofEpochMilli(timestamp / 1_000_000),
173+
ZoneId.systemDefault());
174+
result.append(DATE_FORMATTER.format(dateTime)).append("\t");
175+
return;
176+
}
177+
}
178+
result.append("Unknown time\t");
179+
}
180+
181+
private void appendSeverity(List<Object> row, int index, StringBuilder result) {
182+
if (index >= 0 && index < row.size()) {
183+
result.append(row.get(index)).append("\t");
184+
} else {
185+
result.append("Unknown\t");
186+
}
187+
}
188+
189+
private void appendBody(List<Object> row, int index, StringBuilder result) {
190+
if (index >= 0 && index < row.size()) {
191+
result.append(row.get(index));
192+
} else {
193+
result.append("No content");
194+
}
195+
}
196+
}

0 commit comments

Comments
 (0)