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}