feat: support use_default_attributes for ai-statistics plugin

Add use_default_attributes configuration option to enable sensible defaults.
When use_default_attributes is set to true, plugin automatically applies
a default set of attributes without requiring manual configuration.

Default attributes include:
- messages: extract complete conversation history from request body
- question: auto-extract last user message (built-in attribute)
- answer: auto-extract assistant response (built-in attribute)
- reasoning: auto-extract reasoning process (built-in attribute)
- tool_calls: auto-extract tool calls (built-in attribute)
- reasoning_tokens: auto-extract reasoning token count (built-in attribute)
- cached_tokens: auto-extract cached token count (built-in attribute)
- input_token_details: record complete input token details (built-in attribute)
- output_token_details: record complete output token details (built-in attribute)

Additionally, when use_default_attributes is true:
- value_length_limit is set to 10MB if not specified
- enable_path_suffixes is set to [/completions, /messages] if not specified

This provides sensible defaults for LLM observability out-of-the-box.
This commit is contained in:
johnlanni
2026-02-01 12:37:49 +08:00
parent f288ddf444
commit 8d7524456e

View File

@@ -135,6 +135,54 @@ const (
CtxStreamingToolCallsBuffer = "streamingToolCallsBuffer" CtxStreamingToolCallsBuffer = "streamingToolCallsBuffer"
) )
// getDefaultAttributes returns the default attributes configuration for empty config
func getDefaultAttributes() []Attribute {
return []Attribute{
// Extract complete conversation history from request body
{
Key: "messages",
ValueSource: RequestBody,
Value: "messages",
ApplyToLog: true,
},
// Built-in attributes (no value_source needed, will be auto-extracted)
{
Key: BuiltinQuestionKey,
ApplyToLog: true,
},
{
Key: BuiltinAnswerKey,
ApplyToLog: true,
},
{
Key: BuiltinReasoningKey,
ApplyToLog: true,
},
{
Key: BuiltinToolCallsKey,
ApplyToLog: true,
},
// Token statistics (auto-extracted from response)
{
Key: BuiltinReasoningTokens,
ApplyToLog: true,
},
{
Key: BuiltinCachedTokens,
ApplyToLog: true,
},
// Detailed token information
{
Key: BuiltinInputTokenDetails,
ApplyToLog: true,
},
{
Key: BuiltinOutputTokenDetails,
ApplyToLog: true,
},
}
}
// Default session ID headers in priority order // Default session ID headers in priority order
var defaultSessionHeaders = []string{ var defaultSessionHeaders = []string{
"x-openclaw-session-key", "x-openclaw-session-key",
@@ -361,28 +409,44 @@ func isContentTypeEnabled(contentType string, enabledContentTypes []string) bool
} }
func parseConfig(configJson gjson.Result, config *AIStatisticsConfig) error { func parseConfig(configJson gjson.Result, config *AIStatisticsConfig) error {
// Check if use_default_attributes is enabled
useDefaultAttributes := configJson.Get("use_default_attributes").Bool()
// Parse tracing span attributes setting. // Parse tracing span attributes setting.
attributeConfigs := configJson.Get("attributes").Array() attributeConfigs := configJson.Get("attributes").Array()
// Set value_length_limit
if configJson.Get("value_length_limit").Exists() { if configJson.Get("value_length_limit").Exists() {
config.valueLengthLimit = int(configJson.Get("value_length_limit").Int()) config.valueLengthLimit = int(configJson.Get("value_length_limit").Int())
} else { } else {
config.valueLengthLimit = 4000 config.valueLengthLimit = 4000
} }
config.attributes = make([]Attribute, len(attributeConfigs))
for i, attributeConfig := range attributeConfigs { // Parse attributes or use defaults
attribute := Attribute{} if useDefaultAttributes {
err := json.Unmarshal([]byte(attributeConfig.Raw), &attribute) config.attributes = getDefaultAttributes()
if err != nil { // Update value_length_limit to default when using default attributes
log.Errorf("parse config failed, %v", err) if !configJson.Get("value_length_limit").Exists() {
return err config.valueLengthLimit = 10485760 // 10MB
} }
if attribute.ValueSource == ResponseStreamingBody { log.Infof("Using default attributes configuration")
config.shouldBufferStreamingBody = true } else {
config.attributes = make([]Attribute, len(attributeConfigs))
for i, attributeConfig := range attributeConfigs {
attribute := Attribute{}
err := json.Unmarshal([]byte(attributeConfig.Raw), &attribute)
if err != nil {
log.Errorf("parse config failed, %v", err)
return err
}
if attribute.ValueSource == ResponseStreamingBody {
config.shouldBufferStreamingBody = true
}
if attribute.Rule != "" && attribute.Rule != RuleFirst && attribute.Rule != RuleReplace && attribute.Rule != RuleAppend {
return errors.New("value of rule must be one of [nil, first, replace, append]")
}
config.attributes[i] = attribute
} }
if attribute.Rule != "" && attribute.Rule != RuleFirst && attribute.Rule != RuleReplace && attribute.Rule != RuleAppend {
return errors.New("value of rule must be one of [nil, first, replace, append]")
}
config.attributes[i] = attribute
} }
// Metric settings // Metric settings
config.counterMetrics = make(map[string]proxywasm.MetricCounter) config.counterMetrics = make(map[string]proxywasm.MetricCounter)
@@ -394,14 +458,21 @@ func parseConfig(configJson gjson.Result, config *AIStatisticsConfig) error {
pathSuffixes := configJson.Get("enable_path_suffixes").Array() pathSuffixes := configJson.Get("enable_path_suffixes").Array()
config.enablePathSuffixes = make([]string, 0, len(pathSuffixes)) config.enablePathSuffixes = make([]string, 0, len(pathSuffixes))
for _, suffix := range pathSuffixes { // If use_default_attributes is enabled and enable_path_suffixes is not configured, use default path suffixes
suffixStr := suffix.String() if useDefaultAttributes && !configJson.Get("enable_path_suffixes").Exists() {
if suffixStr == "*" { config.enablePathSuffixes = []string{"/completions", "/messages"}
// Clear the suffixes list since * means all paths are enabled log.Infof("Using default path suffixes: /completions, /messages")
config.enablePathSuffixes = make([]string, 0) } else {
break // Process manually configured path suffixes
for _, suffix := range pathSuffixes {
suffixStr := suffix.String()
if suffixStr == "*" {
// Clear the suffixes list since * means all paths are enabled
config.enablePathSuffixes = make([]string, 0)
break
}
config.enablePathSuffixes = append(config.enablePathSuffixes, suffixStr)
} }
config.enablePathSuffixes = append(config.enablePathSuffixes, suffixStr)
} }
// Parse content type configuration // Parse content type configuration