Skip to content

Commit df74731

Browse files
authored
Merge pull request #27 from GreptimeTeam/feature/audit-logs
feat: audit logging
2 parents 74aff15 + 56526b4 commit df74731

9 files changed

Lines changed: 309 additions & 384 deletions

File tree

README.md

Lines changed: 106 additions & 381 deletions
Large diffs are not rendered by default.

docs/llm-instructions.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# LLM Instructions for GreptimeDB MCP Server
2+
3+
Add this to your system prompt to help AI assistants work with this MCP server.
4+
5+
## System Prompt
6+
7+
```
8+
You have access to a GreptimeDB MCP server for querying and managing time-series data, logs, and metrics.
9+
10+
## Available Tools
11+
- `execute_sql`: Run SQL queries (SELECT, SHOW, DESCRIBE only - read-only access)
12+
- `execute_tql`: Run PromQL-compatible time-series queries
13+
- `query_range`: Time-window aggregation with RANGE/ALIGN syntax
14+
- `describe_table`: Get table schema information
15+
- `health_check`: Check database connection status
16+
- `explain_query`: Analyze query execution plans
17+
18+
### Pipeline Management
19+
- `list_pipelines`: View existing log pipelines
20+
- `create_pipeline`: Create/update pipeline with YAML config (same name creates new version)
21+
- `dryrun_pipeline`: Test pipeline with sample data without writing
22+
- `delete_pipeline`: Remove a pipeline version
23+
24+
**Note**: All HTTP API calls (pipeline tools) require authentication. The MCP server handles auth automatically using configured credentials. When providing curl examples to users, always include `-u <username>:<password>`.
25+
26+
## Available Prompts
27+
Use these prompts for specialized tasks:
28+
- `pipeline_creator`: Generate pipeline YAML from log samples - use when user provides log examples
29+
- `log_pipeline`: Log analysis with full-text search
30+
- `metrics_analysis`: Metrics monitoring and analysis
31+
- `promql_analysis`: PromQL-style queries
32+
- `iot_monitoring`: IoT device data analysis
33+
- `trace_analysis`: Distributed tracing analysis
34+
- `table_operation`: Table diagnostics and optimization
35+
36+
## Workflow Tips
37+
1. For log pipeline creation: Get log sample → use `pipeline_creator` prompt → generate YAML → `create_pipeline` → `dryrun_pipeline` to verify
38+
2. For data analysis: `describe_table` first → understand schema → `execute_sql` or `execute_tql`
39+
3. For time-series: Prefer `query_range` for aggregations, `execute_tql` for PromQL patterns
40+
4. Always check `health_check` if queries fail unexpectedly
41+
```
42+
43+
## Using Prompts in Claude Desktop
44+
45+
In Claude Desktop, you need to add MCP prompts manually:
46+
47+
1. Click the **+** button in the conversation input area
48+
2. Select **MCP Server**
49+
3. Choose **Prompt/References**
50+
4. Select the prompt you want to use (e.g., `pipeline_creator`)
51+
5. Fill in the required arguments
52+
53+
Note: Prompts are not automatically available via `/` slash commands in Claude Desktop. You must add them through the UI as described above.
54+
55+
## Example: Creating a Pipeline
56+
57+
Provide your log sample and ask Claude to create a pipeline:
58+
59+
```
60+
Help me create a GreptimeDB pipeline to parse this nginx log:
61+
127.0.0.1 - - [25/May/2024:20:16:37 +0000] "GET /index.html HTTP/1.1" 200 612 "-" "Mozilla/5.0..."
62+
```
63+
64+
Claude will:
65+
1. Analyze your log format
66+
2. Generate a pipeline YAML configuration
67+
3. Create the pipeline using `create_pipeline` tool
68+
4. Test it with `dryrun_pipeline` tool

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "greptimedb-mcp-server"
7-
version = "0.3.1"
7+
version = "0.4.0"
88
description = "MCP server for GreptimeDB with SQL/TQL/PromQL support, sensitive data masking, and prompt templates for observability data analysis."
99
readme = "README.md"
1010
license = {text = "MIT"}

src/greptimedb_mcp_server/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import sys
23

34
if "-m" not in sys.argv:
@@ -6,7 +7,12 @@
67

78
def main():
89
"""Main entry point for the package."""
9-
server.main()
10+
try:
11+
server.main()
12+
except KeyboardInterrupt:
13+
print("\nReceived Ctrl+C, shutting down...")
14+
except asyncio.CancelledError:
15+
print("\nServer shutdown complete.")
1016

1117

1218
# Expose important items at package level

src/greptimedb_mcp_server/config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ class Config:
7979
MCP HTTP server bind port (for sse/streamable-http transports)
8080
"""
8181

82+
audit_enabled: bool
83+
"""
84+
Enable audit logging for all tool calls
85+
"""
86+
8287
@staticmethod
8388
def from_env_arguments() -> "Config":
8489
"""
@@ -186,6 +191,13 @@ def from_env_arguments() -> "Config":
186191
default=int(os.getenv("GREPTIMEDB_LISTEN_PORT", "8080")),
187192
)
188193

194+
parser.add_argument(
195+
"--audit-enabled",
196+
type=lambda x: x.lower() not in ("false", "0", "no"),
197+
help="Enable audit logging for all tool calls (default: true)",
198+
default=os.getenv("GREPTIMEDB_AUDIT_ENABLED", "true"),
199+
)
200+
189201
args = parser.parse_args()
190202
return Config(
191203
host=args.host,
@@ -202,4 +214,5 @@ def from_env_arguments() -> "Config":
202214
transport=args.transport,
203215
listen_host=args.listen_host,
204216
listen_port=args.listen_port,
217+
audit_enabled=args.audit_enabled,
205218
)

src/greptimedb_mcp_server/server.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
validate_fill,
1313
validate_time_expression,
1414
format_tql_time_param,
15+
audit_log,
1516
)
1617

1718
import asyncio
@@ -857,11 +858,39 @@ def prompt_fn({arg_params}) -> str:
857858
_register_prompts()
858859

859860

861+
def _install_audit_hook():
862+
"""Install audit logging hook by wrapping tool manager's call_tool method."""
863+
original_call_tool = mcp._tool_manager.call_tool
864+
865+
async def audited_call_tool(name, arguments, context=None, convert_result=False):
866+
start_time = time.time()
867+
try:
868+
result = await original_call_tool(name, arguments, context, convert_result)
869+
elapsed_ms = (time.time() - start_time) * 1000
870+
audit_log(name, arguments, success=True, duration_ms=elapsed_ms)
871+
return result
872+
except Exception as e:
873+
elapsed_ms = (time.time() - start_time) * 1000
874+
audit_log(
875+
name, arguments, success=False, duration_ms=elapsed_ms, error=str(e)
876+
)
877+
raise
878+
879+
mcp._tool_manager.call_tool = audited_call_tool
880+
881+
860882
def main():
861883
"""Main entry point."""
862884
global _config
863885
_config = Config.from_env_arguments()
864886

887+
# Install audit logging hook if enabled
888+
if _config.audit_enabled:
889+
_install_audit_hook()
890+
logger.info("Audit logging: enabled")
891+
else:
892+
logger.info("Audit logging: disabled")
893+
865894
# Only configure HTTP server settings for non-stdio transports
866895
# to avoid overriding user's programmatic configuration
867896
if _config.transport != "stdio":

src/greptimedb_mcp_server/utils.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import yaml
44
import os
5+
from typing import Any
56

67
logger = logging.getLogger("greptimedb_mcp_server")
78

@@ -157,10 +158,56 @@ def validate_time_expression(value: str, name: str) -> str:
157158
raise ValueError(f"{name} is required")
158159
if ";" in value or "--" in value:
159160
raise ValueError(f"Invalid characters in {name}")
160-
# Guard against malformed or injected strings with unbalanced quotes
161161
if value.count("'") % 2 != 0:
162162
raise ValueError(f"Unbalanced quotes in {name}")
163163
is_dangerous, reason = security_gate(value)
164164
if is_dangerous:
165165
raise ValueError(f"Dangerous pattern in {name}: {reason}")
166166
return value
167+
168+
169+
# Audit logging
170+
audit_logger = logging.getLogger("greptimedb_mcp_server.audit")
171+
172+
173+
def _truncate_value(v: Any, max_len: int = 200) -> str:
174+
"""Truncate a value to max_len characters."""
175+
v_str = str(v)
176+
if len(v_str) > max_len:
177+
return v_str[:max_len] + "..."
178+
return v_str
179+
180+
181+
def _format_audit_params(params: dict) -> str:
182+
"""Format parameters for audit log."""
183+
if not params:
184+
return ""
185+
parts = []
186+
for k, v in params.items():
187+
parts.append(f'{k}="{_truncate_value(v)}"')
188+
return " | ".join(parts)
189+
190+
191+
def audit_log(
192+
tool: str,
193+
params: dict,
194+
success: bool,
195+
duration_ms: float,
196+
error: str | None = None,
197+
):
198+
"""Record audit log for tool invocation. Never raises exceptions."""
199+
try:
200+
parts = [f"[AUDIT] {tool}"]
201+
202+
params_str = _format_audit_params(params)
203+
if params_str:
204+
parts.append(params_str)
205+
206+
parts.append(f"success={success}")
207+
if error:
208+
parts.append(f'error="{_truncate_value(error)}"')
209+
parts.append(f"duration_ms={duration_ms:.1f}")
210+
211+
audit_logger.info(" | ".join(parts))
212+
except Exception:
213+
pass

tests/test_server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def setup_state():
3939
transport="stdio",
4040
listen_host="0.0.0.0",
4141
listen_port=8080,
42+
audit_enabled=False,
4243
)
4344
# Set global config for get_config() calls
4445
server._config = config

tests/test_utils.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
is_sql_time_expression,
99
format_tql_time_param,
1010
validate_time_expression,
11+
_truncate_value,
12+
_format_audit_params,
13+
audit_log,
1114
)
1215
from greptimedb_mcp_server.formatter import format_results
1316

@@ -523,3 +526,36 @@ def test_validate_time_expression_dangerous():
523526
with pytest.raises(ValueError) as excinfo:
524527
validate_time_expression("DELETE FROM users", "start")
525528
assert "Dangerous pattern" in str(excinfo.value)
529+
530+
531+
# Tests for audit logging functions
532+
533+
534+
def test_truncate_value():
535+
"""Test _truncate_value truncates long values"""
536+
assert _truncate_value("short") == "short"
537+
assert _truncate_value("a" * 201) == "a" * 200 + "..."
538+
539+
540+
def test_format_audit_params():
541+
"""Test _format_audit_params formats params correctly"""
542+
assert _format_audit_params({}) == ""
543+
assert _format_audit_params({"query": "SELECT 1"}) == 'query="SELECT 1"'
544+
545+
546+
def test_audit_log(caplog):
547+
"""Test audit_log records tool calls"""
548+
import logging
549+
550+
with caplog.at_level(logging.INFO, logger="greptimedb_mcp_server.audit"):
551+
audit_log("execute_sql", {"query": "SELECT 1"}, success=True, duration_ms=10.5)
552+
553+
assert len(caplog.records) == 1
554+
msg = caplog.records[0].message
555+
assert "[AUDIT] execute_sql" in msg
556+
assert "success=True" in msg
557+
558+
559+
def test_audit_log_never_raises():
560+
"""Test audit_log never raises exceptions"""
561+
audit_log(None, None, None, None, None) # Should not raise

0 commit comments

Comments
 (0)