Skip to content

Commit 79c3645

Browse files
summary: add string_patterns to promote data event values into summary
Git's trace2 "data" events carry structured (category, key, value) tuples that can contain important diagnostic information, such as curl/SSL error details from gvfs-helper. Previously, these values were only included in the OTLP output at higher detail levels: "dl:process" for process-level data and "dl:verbose" for region-level data. At the default "dl:summary" level, they were silently dropped. This is a problem for deployments that want lightweight telemetry (summary level) but still need visibility into specific error conditions reported via data events. Add a new "string_patterns" rule type to the summary configuration, alongside the existing "message_patterns" and "region_timers". Each rule matches data events by exact category and key prefix, and captures the event's actual value (not just a count) into the summary. This is emitted at all detail levels via the existing "trace2.process.summary" span attribute. Key design decisions: - Values are always emitted as arrays for schema stability, even when only a single event matches. This avoids downstream parsers having to handle both scalar and array types for the same field. - Summary promotion runs before region attachment in apply__data_generic(), so matching is not skipped when region stack lookup fails (e.g. orphaned data events at nesting > 1). - Validation enforces that field names are unique across all three rule types (message_patterns, region_timers, string_patterns). Example configuration: string_patterns: - category: "gvfs-helper" key_prefix: "error/" field_name: "gvfs_helper_errors" For a data event like: {"event":"data", "category":"gvfs-helper", "key":"error/curl", "value":"(curl:35) SSL connect error [hard_fail]"} The summary output will contain: {"gvfs_helper_errors": ["(curl:35) SSL connect error [hard_fail]"]} Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 222a5ba commit 79c3645

4 files changed

Lines changed: 338 additions & 0 deletions

File tree

evt_apply.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,14 @@ func apply__data_generic(tr2 *trace2Dataset, evt *TrEvent) (err error) {
830830
// nesting level n-1 is stored at regionStack[n-2] (assuming the
831831
// Git process properly sets things up).
832832

833+
// Promote matching data values into the summary regardless of
834+
// nesting level. This must run before any early returns below
835+
// so that promotion is not skipped when region attachment fails.
836+
apply__summary_data(tr2,
837+
evt.pm_generic_data.mf_category,
838+
evt.pm_generic_data.mf_key,
839+
evt.pm_generic_data.mf_generic_value)
840+
833841
if evt.pm_generic_data.mf_nesting <= 1 {
834842
tr2.process.setGenericDataValue(evt.pm_generic_data.mf_category,
835843
evt.pm_generic_data.mf_key, evt.pm_generic_data.mf_generic_value)

summary.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ type SummaryAccumulator struct {
1515

1616
// regionTimes maps field names to total time in seconds
1717
regionTimes map[string]float64
18+
19+
// dataValues maps field names to collected values from matching
20+
// data events. Always emitted as an array for schema stability.
21+
dataValues map[string][]interface{}
1822
}
1923

2024
// newSummaryAccumulator creates a new accumulator with
@@ -24,6 +28,7 @@ func newSummaryAccumulator() *SummaryAccumulator {
2428
messageCounts: make(map[string]int64),
2529
regionCounts: make(map[string]int64),
2630
regionTimes: make(map[string]float64),
31+
dataValues: make(map[string][]interface{}),
2732
}
2833
}
2934

@@ -47,6 +52,11 @@ func configuredSummary(settings *SummarySettings) *SummaryAccumulator {
4752
}
4853
}
4954

55+
// Initialize dataValues with field names from StringPatterns
56+
for _, rule := range settings.StringPatterns {
57+
summary.dataValues[rule.FieldName] = nil
58+
}
59+
5060
return summary
5161
}
5262

@@ -68,6 +78,12 @@ func (csa *SummaryAccumulator) addRegionMetrics(countField string, timeField str
6878
}
6979
}
7080

81+
// appendDataValue appends a value from a matching data event to the
82+
// specified field name's value list.
83+
func (csa *SummaryAccumulator) appendDataValue(fieldName string, value interface{}) {
84+
csa.dataValues[fieldName] = append(csa.dataValues[fieldName], value)
85+
}
86+
7187
// toMap converts the accumulated metrics into a single map suitable
7288
// for JSON marshaling. The map contains all non-zero values across
7389
// all metric types (message counts, region counts, region times).
@@ -92,6 +108,12 @@ func (csa *SummaryAccumulator) toMap() map[string]interface{} {
92108
}
93109
}
94110

111+
for fieldName, values := range csa.dataValues {
112+
if len(values) > 0 {
113+
result[fieldName] = values
114+
}
115+
}
116+
95117
return result
96118
}
97119

@@ -153,3 +175,29 @@ func apply__summary_region(tr2 *trace2Dataset, region *TrRegion) {
153175
}
154176
}
155177
}
178+
179+
// apply__summary_data checks if a data event matches any
180+
// configured data pattern rules and appends the event's value
181+
// to the summary if a match is found. This promotes matching
182+
// data event values into the summary regardless of nesting level
183+
// or detail level.
184+
func apply__summary_data(tr2 *trace2Dataset, category string, key string, value interface{}) {
185+
if tr2.process.summary == nil {
186+
return
187+
}
188+
189+
if tr2.rcvr_base == nil || tr2.rcvr_base.RcvrConfig == nil {
190+
return
191+
}
192+
193+
css := tr2.rcvr_base.RcvrConfig.summary
194+
if css == nil {
195+
return
196+
}
197+
198+
for _, rule := range css.StringPatterns {
199+
if category == rule.Category && strings.HasPrefix(key, rule.KeyPrefix) {
200+
tr2.process.summary.appendDataValue(rule.FieldName, value)
201+
}
202+
}
203+
}

summary_settings.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
type SummarySettings struct {
1111
MessagePatterns []MessagePatternRule `mapstructure:"message_patterns"`
1212
RegionTimers []RegionTimerRule `mapstructure:"region_timers"`
13+
StringPatterns []StringPatternRule `mapstructure:"string_patterns"`
1314
}
1415

1516
// MessagePatternRule defines a rule for counting messages that match
@@ -46,6 +47,24 @@ type RegionTimerRule struct {
4647
TimeField string `mapstructure:"time_field"`
4748
}
4849

50+
// StringPatternRule defines a rule for promoting values from data events
51+
// that match a specific (category, key prefix) pair. When a data event's
52+
// category matches exactly and its key starts with the specified prefix,
53+
// the event's value is captured and emitted in the summary regardless of
54+
// detail level. Multiple matching values are collected into an array.
55+
type StringPatternRule struct {
56+
// Category is the data event category to match (exact match)
57+
Category string `mapstructure:"category"`
58+
59+
// KeyPrefix is the string prefix to match at the beginning of
60+
// the data event's key field
61+
KeyPrefix string `mapstructure:"key_prefix"`
62+
63+
// FieldName is the name of the field in the summary object
64+
// where matched values will be stored (always as an array)
65+
FieldName string `mapstructure:"field_name"`
66+
}
67+
4968
// parseSummarySettings parses a summary configuration
5069
// from a YML file and validates the configuration.
5170
func parseSummarySettings(path string) (*SummarySettings, error) {
@@ -104,5 +123,22 @@ func parseSummarySettingsFromBuffer(data []byte, path string) (*SummarySettings,
104123
}
105124
}
106125

126+
// Validate string pattern rules
127+
for i, rule := range css.StringPatterns {
128+
if len(rule.Category) == 0 {
129+
return nil, fmt.Errorf("string_patterns[%d]: category cannot be empty", i)
130+
}
131+
if len(rule.KeyPrefix) == 0 {
132+
return nil, fmt.Errorf("string_patterns[%d]: key_prefix cannot be empty", i)
133+
}
134+
if len(rule.FieldName) == 0 {
135+
return nil, fmt.Errorf("string_patterns[%d]: field_name cannot be empty", i)
136+
}
137+
if fieldNames[rule.FieldName] {
138+
return nil, fmt.Errorf("string_patterns[%d]: duplicate field_name '%s'", i, rule.FieldName)
139+
}
140+
fieldNames[rule.FieldName] = true
141+
}
142+
107143
return css, nil
108144
}

0 commit comments

Comments
 (0)