001/*
002 *  Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.agentsflex.core.model.chat.tool;
017
018
019import com.agentsflex.core.observability.Observability;
020import com.alibaba.fastjson2.JSON;
021import io.opentelemetry.api.common.Attributes;
022import io.opentelemetry.api.common.AttributesBuilder;
023import io.opentelemetry.api.metrics.DoubleHistogram;
024import io.opentelemetry.api.metrics.LongCounter;
025import io.opentelemetry.api.metrics.Meter;
026import io.opentelemetry.api.trace.Span;
027import io.opentelemetry.api.trace.StatusCode;
028import io.opentelemetry.api.trace.Tracer;
029import io.opentelemetry.context.Scope;
030
031import java.io.File;
032import java.io.InputStream;
033import java.util.Map;
034import java.util.regex.Pattern;
035
036/**
037 * 增强版工具可观测性拦截器,支持:
038 * - 全局/工具级开关
039 * - JSON 结构化参数与结果
040 * - 自动脱敏敏感字段
041 * - 错误类型分类
042 * - 类型安全结果处理
043 */
044public class ToolObservabilityInterceptor implements ToolInterceptor {
045
046    private static final Tracer TRACER = Observability.getTracer();
047    private static final Meter METER = Observability.getMeter();
048
049    private static final LongCounter TOOL_CALL_COUNT = METER.counterBuilder("tool.call.count")
050        .setDescription("Total number of tool calls")
051        .build();
052
053    private static final DoubleHistogram TOOL_LATENCY_HISTOGRAM = METER.histogramBuilder("tool.call.latency")
054        .setDescription("Tool call latency in seconds")
055        .setUnit("s")
056        .build();
057
058    private static final LongCounter TOOL_ERROR_COUNT = METER.counterBuilder("tool.call.error.count")
059        .setDescription("Total number of tool call errors")
060        .build();
061
062    // 长度限制(OpenTelemetry 推荐单个 attribute ≤ 12KB,此处保守)
063    private static final int MAX_JSON_LENGTH_FOR_SPAN = 4000;
064
065    // 敏感字段正则(匹配 key,不区分大小写)
066    private static final Pattern SENSITIVE_KEY_PATTERN = Pattern.compile(
067        ".*(password|token|secret|key|auth|credential|cert|session).*",
068        Pattern.CASE_INSENSITIVE
069    );
070
071
072    @Override
073    public Object intercept(ToolContext context, ToolChain chain) throws Exception {
074        Tool tool = context.getTool();
075        String toolName = tool.getName();
076
077        // 动态开关:全局关闭 或 工具在黑名单中
078        if (!Observability.isEnabled() || Observability.isToolExcluded(toolName)) {
079            return chain.proceed(context);
080        }
081
082        Span span = TRACER.spanBuilder("tool." + toolName)
083            .setAttribute("tool.name", toolName)
084            .startSpan();
085
086        long startTimeNanos = System.nanoTime();
087
088        try (Scope ignored = span.makeCurrent()) {
089
090            // 记录脱敏后的参数(JSON)
091            Map<String, Object> args = context.getArgsMap();
092            if (args != null && !args.isEmpty()) {
093                String safeArgsJson = safeToJson(args);
094                span.setAttribute("tool.arguments", safeArgsJson);
095            }
096
097            // 执行工具
098            Object result = chain.proceed(context);
099
100            // 记录结果(成功)
101            if (result != null) {
102                String safeResult = safeToString(result);
103                if (safeResult.length() > MAX_JSON_LENGTH_FOR_SPAN) {
104                    safeResult = safeResult.substring(0, MAX_JSON_LENGTH_FOR_SPAN) + "...";
105                }
106                span.setAttribute("tool.result", safeResult);
107            }
108
109            recordMetrics(toolName, true, null, startTimeNanos);
110            return result;
111
112        } catch (Exception e) {
113            recordError(span, e, toolName, startTimeNanos);
114            throw e;
115        } finally {
116            span.end();
117        }
118    }
119
120    // 安全转为 JSON,自动脱敏
121    private String safeToJson(Object obj) {
122        try {
123            String json = JSON.toJSONString(obj);
124            return redactSensitiveFields(json);
125        } catch (Exception e) {
126            return "[JSON_SERIALIZATION_ERROR]";
127        }
128    }
129
130    // 简单脱敏:将敏感 key 对应的 value 替换为 "***"
131    private String redactSensitiveFields(String json) {
132        // 简单实现:按行处理(适用于格式化 JSON)
133        // 更严谨可用 JSON parser 遍历,但性能低;此处平衡安全与性能
134        String[] lines = json.split("\n");
135        for (int i = 0; i < lines.length; i++) {
136            String line = lines[i];
137            int colon = line.indexOf(':');
138            if (colon > 0) {
139                String keyPart = line.substring(0, colon);
140                if (SENSITIVE_KEY_PATTERN.matcher(keyPart).matches()) {
141                    int valueStart = colon + 1;
142                    // 找到 value 开始和结束(简单处理 string/value)
143                    if (valueStart < line.length()) {
144                        char firstChar = line.charAt(valueStart);
145                        if (firstChar == '"' || firstChar == '\'') {
146                            // 字符串值
147                            int endQuote = line.indexOf(firstChar, valueStart + 1);
148                            if (endQuote > valueStart) {
149                                lines[i] = line.substring(0, valueStart + 1) + "***" + line.substring(endQuote);
150                            }
151                        } else {
152                            // 非字符串值(截至逗号或行尾)
153                            int endValue = line.indexOf(',', valueStart);
154                            if (endValue == -1) endValue = line.length();
155                            lines[i] = line.substring(0, valueStart) + " \"***\"" + line.substring(endValue);
156                        }
157                    }
158                }
159            }
160        }
161        return String.join("\n", lines);
162    }
163
164    // 类型安全的 toString
165    private String safeToString(Object obj) {
166        if (obj == null) {
167            return "null";
168        }
169        if (obj instanceof byte[]) {
170            return "[binary_data]";
171        }
172        if (obj instanceof InputStream || obj instanceof File) {
173            return "[stream_or_file]";
174        }
175        if (obj instanceof Map || obj instanceof Iterable) {
176            return safeToJson(obj);
177        }
178        return obj.toString();
179    }
180
181    private void recordMetrics(String toolName, boolean success, String errorType, long startTimeNanos) {
182        double latencySeconds = (System.nanoTime() - startTimeNanos) / 1_000_000_000.0;
183        AttributesBuilder builder = Attributes.builder()
184            .put("tool.name", toolName)
185            .put("tool.success", success);
186        if (errorType != null) {
187            builder.put("error.type", errorType);
188        }
189        Attributes attrs = builder.build();
190
191        TOOL_CALL_COUNT.add(1, attrs);
192        TOOL_LATENCY_HISTOGRAM.record(latencySeconds, attrs);
193        if (!success) {
194            TOOL_ERROR_COUNT.add(1, attrs);
195        }
196    }
197
198    private void recordError(Span span, Exception e, String toolName, long startTimeNanos) {
199        span.setStatus(StatusCode.ERROR, e.getMessage());
200        span.recordException(e);
201
202        // 错误分类:业务异常(可预期) vs 系统异常(不可预期)
203        String errorType = e instanceof RuntimeException && !(e instanceof IllegalStateException) ? "business" : "system";
204        recordMetrics(toolName, false, errorType, startTimeNanos);
205    }
206}