Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion ai/prompts/agentAct.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,16 @@ Tool selection policy

Important: You must not answer anything in natural language.
Always call one of the provided tools if applicable.
If you cannot find a suitable tool, return an empty tool call.
If you cannot find a suitable tool, return an empty tool call.

Observability tool policy
- Call `get_observability_capabilities` before the first metrics or trace query when provider availability is unknown.
- Use `query_prometheus` for a current snapshot and `query_prometheus_range` for trends. Range queries must stay within 24 hours and use a step of at least 15 seconds.
- Use metric and label names that exist in Dubbo Admin dashboards. Important labels include `application_name`, `instance`, `pod`, `interface`, `method`, and `version`.
- Common provider metrics include `dubbo_provider_qps_total`, `dubbo_provider_requests_total`, `dubbo_provider_requests_succeed_total`, `dubbo_provider_requests_failed_total`, `dubbo_provider_requests_timeout_total`, `dubbo_provider_requests_processing`, `dubbo_provider_rt_avg_milliseconds_aggregate`, `dubbo_provider_rt_milliseconds_p95`, and `dubbo_provider_rt_milliseconds_p99`.
- Equivalent consumer metrics use the `dubbo_consumer_` prefix. Thread-pool signals include `dubbo_thread_pool_active_size`, `dubbo_thread_pool_core_size`, and `dubbo_thread_pool_queue_size`.
- Counters must be evaluated with `rate` or `increase` over a bounded window. For success rate, divide successful requests by total requests and protect against an empty denominator.
- Begin with an application-level aggregation, then narrow to `instance` or `pod`; narrow to `interface` and `method` only when service-level evidence requires it.
- Do not repeat an identical query after a successful response. Change the dimension or time window only to test a stated hypothesis.
- Call `get_trace_by_id` only with a TraceID supplied by the user or returned by another tool. Use parentSpanId, serviceName, durationMicros, and error status to locate propagation and bottlenecks.
- Never invent metric names, label values, TraceIDs, provider names, or tool results.
5 changes: 5 additions & 0 deletions ai/prompts/agentObserve.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ Given conversation history and tool outputs, return a short structured decision.
- Set `heartbeat=false` when current evidence is enough to answer the user.
- If the same or similar RAG/tool search failed multiple times, stop and answer with available knowledge.
- Do not repeat long raw tool content.
- For metrics, cite the evaluated time or range, key labels, and values that support the conclusion.
- For traces, cite the relevant service, operation, span ID, parent span ID, duration, and error status.
- Distinguish explicitly between no data, provider/query failure, and evidence of a healthy state.
- A single metric spike is not a root cause. Continue when logs, trace evidence, or a comparison dimension is still needed to validate the hypothesis.
- Stop when the available metrics and trace evidence identify the affected scope and the most likely failure boundary, or when configured observability cannot provide further evidence.

# Latest-Turn Rules (STRICT)
- You must answer the latest user message in this session only.
Expand Down
9 changes: 9 additions & 0 deletions ai/prompts/agentThink.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ Choose domain by question:
- Dubbo topics -> Dubbo docs tools
- General topics -> pick the most relevant domain first

# Observability Diagnosis Rules
- For live Dubbo incidents, prefer runtime evidence over documentation retrieval.
- If observability availability is unknown, suggest `get_observability_capabilities` first.
- For metric questions, use `query_prometheus` for the current state, then `query_prometheus_range` only when a trend or incident window is needed.
- When the user supplies a TraceID, or a log result exposes one, suggest `get_trace_by_id` before guessing where the failure occurred.
- Correlate evidence in this order when available: service metadata -> metrics -> logs -> trace spans.
- Treat missing data, tool failure, and healthy data as different outcomes. Missing data is not evidence of health.
- Prefer narrow application, instance, interface, and method filters. Never suggest an unbounded high-cardinality PromQL query.

# Output Constraint
Return JSON only. No markdown, no code fence, no extra text.

Expand Down
10 changes: 10 additions & 0 deletions app/dubbo-admin/dubbo-admin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ observability:
# type: loki
# endpoint: http://localhost:3100
# tenant: ""
# [Optional] trace providers used by MCP diagnosis tools.
# The bundled monitoring manifests use the Jaeger query API on port 16686.
# tracing:
# defaultProvider: jaeger-main
# providers:
# - name: jaeger-main
# type: jaeger
# endpoint: http://jaeger.monitoring.svc:16686
# bearerToken: ""
# tenant: ""

# [Optional] config for console.
console:
Expand Down
14 changes: 14 additions & 0 deletions pkg/config/observability/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type Config struct {
Prometheus string `json:"prometheus" yaml:"prometheus"`
// Logs configures the log query provider.
Logs *LogsConfig `json:"logs,omitempty" yaml:"logs,omitempty"`
// Tracing configures trace query providers used by diagnostic tools.
Tracing *TracingConfig `json:"tracing,omitempty" yaml:"tracing,omitempty"`

GrafanaBaseURL *url.URL `json:"-" yaml:"-"`
PrometheusBaseURL *url.URL `json:"-" yaml:"-"`
Expand Down Expand Up @@ -62,9 +64,21 @@ func (c *Config) Validate() error {
return err
}
}
if c.Tracing != nil {
if err := c.Tracing.Validate(); err != nil {
return err
}
}
return nil
}

func (c *Config) Sanitize() {
if c == nil || c.Tracing == nil {
return
}
c.Tracing.Sanitize()
}

func DefaultObservabilityConfig() *Config {
return &Config{}
}
133 changes: 133 additions & 0 deletions pkg/config/observability/tracing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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 observability

import (
"fmt"
"net/url"

"github.com/duke-git/lancet/v2/strutil"

"github.com/apache/dubbo-admin/pkg/common/bizerror"
"github.com/apache/dubbo-admin/pkg/config"
)

// TraceProviderType identifies a supported trace query backend.
type TraceProviderType string

const (
// TraceProviderJaeger queries traces through the Jaeger query API.
TraceProviderJaeger TraceProviderType = "jaeger"
)

// TracingConfig selects the default trace provider and contains provider definitions.
type TracingConfig struct {
DefaultProvider string `json:"defaultProvider" yaml:"defaultProvider"`
Providers []TraceProviderConfig `json:"providers" yaml:"providers"`
}

// TraceProviderConfig contains connection and optional authentication settings for one backend.
type TraceProviderConfig struct {
Name string `json:"name" yaml:"name"`
Type TraceProviderType `json:"type" yaml:"type"`
Endpoint string `json:"endpoint" yaml:"endpoint"`
BearerToken string `json:"bearerToken,omitempty" yaml:"bearerToken,omitempty"`
Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"`
}

// Validate verifies provider uniqueness, endpoint safety, and default-provider selection.
func (c *TracingConfig) Validate() error {
if c == nil || len(c.Providers) == 0 {
return nil
}
if strutil.IsBlank(c.DefaultProvider) {
return bizerror.New(bizerror.ConfigError, "default trace provider is required")
}

seen := make(map[string]struct{}, len(c.Providers))
foundDefault := false
for _, provider := range c.Providers {
if strutil.IsBlank(provider.Name) {
return bizerror.New(bizerror.ConfigError, "trace provider name is required")
}
if _, ok := seen[provider.Name]; ok {
return bizerror.New(bizerror.ConfigError, fmt.Sprintf("duplicate trace provider name: %s", provider.Name))
}
seen[provider.Name] = struct{}{}
if provider.Name == c.DefaultProvider {
foundDefault = true
}
if provider.Type != TraceProviderJaeger {
return bizerror.New(bizerror.ConfigError, fmt.Sprintf("unsupported trace provider type: %s", provider.Type))
}
if err := validateTraceEndpoint(provider.Endpoint); err != nil {
return err
}
}
if !foundDefault {
return bizerror.New(bizerror.ConfigError, fmt.Sprintf("default trace provider %q is not configured", c.DefaultProvider))
}
return nil
}

func validateTraceEndpoint(endpoint string) error {
if strutil.IsBlank(endpoint) {
return bizerror.New(bizerror.ConfigError, "trace provider endpoint is required")
}
parsed, err := url.Parse(endpoint)
if err != nil {
return bizerror.Wrap(err, bizerror.ConfigError, fmt.Sprintf("invalid trace provider endpoint: %s", endpoint))
}
if (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
return bizerror.New(bizerror.ConfigError, fmt.Sprintf("invalid trace provider endpoint: %s", endpoint))
}
return nil
}

// Default returns the configured default trace provider.
func (c *TracingConfig) Default() (TraceProviderConfig, bool) {
if c == nil {
return TraceProviderConfig{}, false
}
return c.Get(c.DefaultProvider)
}

// Get returns a named trace provider.
func (c *TracingConfig) Get(name string) (TraceProviderConfig, bool) {
if c == nil {
return TraceProviderConfig{}, false
}
for _, provider := range c.Providers {
if provider.Name == name {
return provider, true
}
}
return TraceProviderConfig{}, false
}

// Sanitize masks provider credentials before configuration is displayed.
func (c *TracingConfig) Sanitize() {
if c == nil {
return
}
for i := range c.Providers {
if c.Providers[i].BearerToken != "" {
c.Providers[i].BearerToken = config.SanitizedValue
}
}
}
79 changes: 79 additions & 0 deletions pkg/config/observability/tracing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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 observability

import (
"testing"

"github.com/apache/dubbo-admin/pkg/config"
)

func TestTracingConfigValidate(t *testing.T) {
cfg := &TracingConfig{
DefaultProvider: "jaeger-main",
Providers: []TraceProviderConfig{{
Name: "jaeger-main", Type: TraceProviderJaeger, Endpoint: "http://jaeger:16686",
}},
}
if err := cfg.Validate(); err != nil {
t.Fatalf("expected valid tracing config, got %v", err)
}

cfg.Providers[0].Endpoint = "jaeger:16686"
if err := cfg.Validate(); err == nil {
t.Fatal("expected endpoint without scheme to be rejected")
}
}

func TestTracingConfigRejectsDuplicateAndMissingDefault(t *testing.T) {
cfg := &TracingConfig{
DefaultProvider: "missing",
Providers: []TraceProviderConfig{
{Name: "same", Type: TraceProviderJaeger, Endpoint: "http://jaeger:16686"},
{Name: "same", Type: TraceProviderJaeger, Endpoint: "http://jaeger-2:16686"},
},
}
if err := cfg.Validate(); err == nil {
t.Fatal("expected duplicate provider names to be rejected")
}

cfg.Providers = cfg.Providers[:1]
if err := cfg.Validate(); err == nil {
t.Fatal("expected missing default provider to be rejected")
}
}

func TestObservabilitySanitizeTraceToken(t *testing.T) {
cfg := &Config{Tracing: &TracingConfig{Providers: []TraceProviderConfig{{BearerToken: "secret"}}}}
cfg.Sanitize()
if got := cfg.Tracing.Providers[0].BearerToken; got != config.SanitizedValue {
t.Fatalf("expected sanitized token, got %q", got)
}
}

func TestTracingConfigRejectsUnsupportedProvider(t *testing.T) {
cfg := &TracingConfig{
DefaultProvider: "tempo-main",
Providers: []TraceProviderConfig{{
Name: "tempo-main", Type: TraceProviderType("tempo"), Endpoint: "http://tempo:3200",
}},
}
if err := cfg.Validate(); err == nil {
t.Fatal("expected unsupported provider to be rejected")
}
}
29 changes: 29 additions & 0 deletions pkg/mcp/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,32 @@ const (
- 使用 `common.JsonResult` 返回结果
- 使用 `common.ErrorResult` 处理错误
- 使用 `common.DefaultPageSize` 和 `common.DefaultPageNumber` 常量

## 可观测性诊断工具

Dubbo Admin MCP 提供以下只读诊断工具:

| 工具 | 数据源 | 用途 |
|------|--------|------|
| `query_prometheus` | Prometheus | 执行即时 PromQL 查询 |
| `query_prometheus_range` | Prometheus | 执行最长 24 小时的区间 PromQL 查询 |
| `get_trace_by_id` | Jaeger | 按 TraceID 查询规范化调用链 |
| `get_observability_capabilities` | 本地配置 | 查询数据源和查询限制 |

Prometheus 工具复用 `observability.prometheus`。区间查询的 `step` 不得低于 15 秒,返回结果最多包含 100 个时间序列和 5000 个采样点。结果被裁剪时,响应中的 `truncated` 为 `true`。

Jaeger 查询需要配置 trace provider:

```yaml
observability:
tracing:
defaultProvider: jaeger-main
providers:
- name: jaeger-main
type: jaeger
endpoint: http://jaeger.monitoring.svc:16686
bearerToken: "" # 可选
tenant: "" # 可选,通过 X-Scope-OrgID 发送
```

诊断工具不会记录完整 PromQL、Bearer Token 或完整 trace attributes。工具只读,不提供流量、配置或部署变更能力。
42 changes: 42 additions & 0 deletions pkg/mcp/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"github.com/apache/dubbo-admin/pkg/mcp/common"
"github.com/apache/dubbo-admin/pkg/mcp/tools"
logtools "github.com/apache/dubbo-admin/pkg/mcp/tools/log"
metrictools "github.com/apache/dubbo-admin/pkg/mcp/tools/metrics"
tracetools "github.com/apache/dubbo-admin/pkg/mcp/tools/trace"
)

// RegisterTools 注册所有 MCP 工具
Expand Down Expand Up @@ -370,4 +372,44 @@ func RegisterTools(server *Server) {
},
Handler: logtools.GetLogCapabilities,
})

server.RegisterTool(&common.ToolDef{
Name: "query_prometheus",
Description: "Execute an instant PromQL query and return normalized metric series",
InputSchema: common.InputSchema{
Type: "object",
Properties: metrictools.InstantProperties(),
Required: []string{"query"},
},
Handler: metrictools.QueryPrometheus,
})

server.RegisterTool(&common.ToolDef{
Name: "query_prometheus_range",
Description: "Execute a bounded PromQL range query for trend and incident-window analysis",
InputSchema: common.InputSchema{
Type: "object",
Properties: metrictools.RangeProperties(),
Required: []string{"query", "startTime", "endTime", "step"},
},
Handler: metrictools.QueryPrometheusRange,
})

server.RegisterTool(&common.ToolDef{
Name: "get_trace_by_id",
Description: "Query a distributed trace by TraceID and return services, span relationships, latency, and error status",
InputSchema: common.InputSchema{
Type: "object",
Properties: tracetools.Properties(),
Required: []string{"traceId"},
},
Handler: tracetools.GetTraceByID,
})

server.RegisterTool(&common.ToolDef{
Name: "get_observability_capabilities",
Description: "Report Prometheus and trace providers, capabilities, and enforced query limits",
InputSchema: common.InputSchema{Type: "object"},
Handler: tools.GetObservabilityCapabilities,
})
}
Loading