Skip to content
Merged
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
3 changes: 3 additions & 0 deletions Docs/Examples/summary_example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ region_timers:
# "batchTime": 12.3
# }
# }
#
# To also capture specific data event values verbatim in the summary,
# add important_events rules to your filter.yml instead.
47 changes: 47 additions & 0 deletions Docs/config-filter-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,47 @@ the `ruleset_key`.)



## Important Events

In addition to controlling verbosity, the `filter.yml` file can
declare a list of data events that should always be captured verbatim,
regardless of the active detail level. This lets operators guarantee
that specific Trace2 data values are always surfaced in the OTEL
process span even when verbose telemetry is disabled.
Comment on lines +259 to +263
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this description of the feature - fits well!


Each rule matches data events by `category` (exact match) and
`key_prefix` (prefix match on the event's key field). All matching
values are collected into an array under the specified `field_name` in
the `trace2.process.important_events` span attribute.

```
important_events:
- category: <category-string>
key_prefix: <key-prefix-string>
field_name: <field-name>
...
```

For example, to always capture error details from a `gvfs-helper`
subprocess regardless of how verbosity is configured:

```
important_events:
- category: "gvfs-helper"
key_prefix: "error/"
field_name: "gvfs_helper_errors"
```

This would produce the following in the OTEL process span:

```
"trace2.process.important_events": {
"gvfs_helper_errors": ["(curl:35) SSL connect error [hard_fail]"]
}
```



## Filter Settings Syntax

Now that all of the concepts have been introduced, we can describe
Expand All @@ -277,6 +318,12 @@ rulesets:

defaults:
ruleset: <ruleset-name> | <detail-level>

important_events:
- category: <category-string>
key_prefix: <key-prefix-string>
field_name: <summary-field-name>
...
```

The value of the `defaults.ruleset` parameter will be used when a Git
Expand Down
21 changes: 21 additions & 0 deletions Docs/configure-custom-collector.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ receivers:
pipe: <windows-named-pipe-pathname>
pii: <pii-settings-pathname>
filter: <filter-settings-pathname>
summary: <summary-settings-pathname>
```

For example:
Expand All @@ -57,6 +58,7 @@ receivers:
pipe: "//./pipe/my-collector.pipe"
pii: "/usr/local/my-collector/pii.yml"
filter: "/usr/local/my-collector/filter.yml"
summary: "/usr/local/my-collector/summary.yml"
```

### `<unix-domain-socket-pathname>` (Required on Unix)
Expand Down Expand Up @@ -117,3 +119,22 @@ generated OTEL telemetry data. This is optional. If omitted,
summary-level telemetry will be emitted.

See [config filter settings](./config-filter-settings.md) for details.

### `<summary-settings-pathname>` (Optional)

The pathname to a `summary.yml` file controlling which trace2 events
are aggregated into the `trace2.process.summary` attribute on the OTEL
process span. This is optional. If omitted, no aggregated summary
metrics are emitted.

The summary is emitted at all detail levels (including `dl:summary`),
making it useful for surfacing aggregated statistics without requiring
verbose telemetry.

See the [summary example](./Examples/summary_example.yml) for a
complete example configuration.

To capture specific data event values verbatim (emitted in a separate
`trace2.process.important_events` span attribute), use the
`important_events` section of the filter settings. See
[config filter settings](./config-filter-settings.md) for details.
10 changes: 9 additions & 1 deletion evt_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,14 @@ func apply__data_generic(tr2 *trace2Dataset, evt *TrEvent) (err error) {
// nesting level n-1 is stored at regionStack[n-2] (assuming the
// Git process properly sets things up).

// Capture matching data values into importantEvents regardless of
// nesting level. This must run before any early returns below
// so that capture is not skipped when region attachment fails.
apply__important_events(tr2,
evt.pm_generic_data.mf_category,
evt.pm_generic_data.mf_key,
evt.pm_generic_data.mf_generic_value)

if evt.pm_generic_data.mf_nesting <= 1 {
tr2.process.setGenericDataValue(evt.pm_generic_data.mf_category,
evt.pm_generic_data.mf_key, evt.pm_generic_data.mf_generic_value)
Expand All @@ -844,7 +852,7 @@ func apply__data_generic(tr2 *trace2Dataset, evt *TrEvent) (err error) {
return nil
}
rWant := evt.pm_generic_data.mf_nesting - 2
if int64(len(th.regionStack)) < rWant {
if rWant < 0 || int64(len(th.regionStack)) <= rWant {
// TODO log debug warning.
return nil
}
Expand Down
71 changes: 67 additions & 4 deletions filter_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,36 @@ import (
// look for in the Trace2 event stream to help us decide how to
// filter data for a particular command.
type FilterSettings struct {
Keynames FilterKeynames `mapstructure:"keynames"`
Nicknames FilterNicknames `mapstructure:"nicknames"`
Rulesets FilterRulesets `mapstructure:"rulesets"`
Defaults FilterDefaults `mapstructure:"defaults"`
Keynames FilterKeynames `mapstructure:"keynames"`
Nicknames FilterNicknames `mapstructure:"nicknames"`
Rulesets FilterRulesets `mapstructure:"rulesets"`
Defaults FilterDefaults `mapstructure:"defaults"`
ImportantEvents []ImportantEventRule `mapstructure:"important_events"`

// The set of custom rulesets defined in YML are each parsed
// and loaded into definitions so that we can use them.
rulesetDefs map[string]*RulesetDefinition
}

// ImportantEventRule defines a rule for promoting values from data events
// that match a specific (category, key prefix) pair into the process
// summary, regardless of the active detail level. This lets operators
// guarantee that certain data event values are always captured and
// surfaced in the OTEL process span even when verbose telemetry is
// disabled. Multiple matching values are collected into an array.
type ImportantEventRule struct {
// Category is the data event category to match (exact match)
Category string `mapstructure:"category"`

// KeyPrefix is the string prefix to match at the beginning of
// the data event's key field
KeyPrefix string `mapstructure:"key_prefix"`

// FieldName is the name of the field in the summary object
// where matched values will be stored (always as an array)
FieldName string `mapstructure:"field_name"`
}

// FilterKeynames defines the names of the Git config settings that
// will be used in `def_param` events to send repository/worktree
// data to us. This lets a site have their own namespace for
Expand Down Expand Up @@ -100,9 +120,52 @@ func parseFilterSettingsFromBuffer(data []byte, path string) (*FilterSettings, e
}
}

fieldNames := make(map[string]bool)
for i, rule := range fs.ImportantEvents {
if len(rule.Category) == 0 {
return nil, fmt.Errorf("important_events[%d]: category cannot be empty", i)
}
if len(rule.KeyPrefix) == 0 {
return nil, fmt.Errorf("important_events[%d]: key_prefix cannot be empty", i)
}
if len(rule.FieldName) == 0 {
return nil, fmt.Errorf("important_events[%d]: field_name cannot be empty", i)
}
if fieldNames[rule.FieldName] {
return nil, fmt.Errorf("important_events[%d]: duplicate field_name '%s'", i, rule.FieldName)
}
fieldNames[rule.FieldName] = true
}

return fs, nil
}

// apply__important_events checks if a data event matches any configured
// important_events rules and appends the event's value to the
// importantEvents map if a match is found. Matching events are captured
// regardless of nesting level or detail level.
func apply__important_events(tr2 *trace2Dataset, category string, key string, value interface{}) {
if tr2.process.importantEvents == nil {
return
}

if tr2.rcvr_base == nil || tr2.rcvr_base.RcvrConfig == nil {
return
}

fs := tr2.rcvr_base.RcvrConfig.filterSettings
if fs == nil {
return
}

for _, rule := range fs.ImportantEvents {
if category == rule.Category && strings.HasPrefix(key, rule.KeyPrefix) {
tr2.process.importantEvents[rule.FieldName] = append(
tr2.process.importantEvents[rule.FieldName], value)
}
}
}

// Add a ruleset to the filter settings. This is primarily for writing test code.
func (fs *FilterSettings) addRuleset(rs_name string, path string, rsdef *RulesetDefinition) {
if fs.Rulesets == nil {
Expand Down
75 changes: 75 additions & 0 deletions filter_settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,81 @@ func x_TryLoadRuleset(t *testing.T, fs *FilterSettings, name string, path string

// //////////////////////////////////////////////////////////////

// //////////////////////////////////////////////////////////////
// important_events validation tests

func Test_ImportantEvents_Valid(t *testing.T) {
yml := `
important_events:
- category: "gvfs-helper"
key_prefix: "error/"
field_name: "gvfs_helper_errors"
- category: "network"
key_prefix: "timeout/"
field_name: "network_timeouts"
`
fs, err := parseFilterSettingsFromBuffer([]byte(yml), "test.yml")
assert.NoError(t, err)
assert.NotNil(t, fs)
assert.Equal(t, 2, len(fs.ImportantEvents))
assert.Equal(t, "gvfs-helper", fs.ImportantEvents[0].Category)
assert.Equal(t, "error/", fs.ImportantEvents[0].KeyPrefix)
assert.Equal(t, "gvfs_helper_errors", fs.ImportantEvents[0].FieldName)
}

func Test_ImportantEvents_EmptyCategory_Rejected(t *testing.T) {
yml := `
important_events:
- category: ""
key_prefix: "error/"
field_name: "errors"
`
_, err := parseFilterSettingsFromBuffer([]byte(yml), "test.yml")
assert.Error(t, err)
assert.Contains(t, err.Error(), "category cannot be empty")
}

func Test_ImportantEvents_EmptyKeyPrefix_Rejected(t *testing.T) {
yml := `
important_events:
- category: "gvfs-helper"
key_prefix: ""
field_name: "errors"
`
_, err := parseFilterSettingsFromBuffer([]byte(yml), "test.yml")
assert.Error(t, err)
assert.Contains(t, err.Error(), "key_prefix cannot be empty")
}

func Test_ImportantEvents_EmptyFieldName_Rejected(t *testing.T) {
yml := `
important_events:
- category: "gvfs-helper"
key_prefix: "error/"
field_name: ""
`
_, err := parseFilterSettingsFromBuffer([]byte(yml), "test.yml")
assert.Error(t, err)
assert.Contains(t, err.Error(), "field_name cannot be empty")
}

func Test_ImportantEvents_DuplicateFieldName_Rejected(t *testing.T) {
yml := `
important_events:
- category: "gvfs-helper"
key_prefix: "error/"
field_name: "shared_name"
- category: "network"
key_prefix: "timeout/"
field_name: "shared_name"
`
_, err := parseFilterSettingsFromBuffer([]byte(yml), "test.yml")
assert.Error(t, err)
assert.Contains(t, err.Error(), "duplicate field_name")
}

// //////////////////////////////////////////////////////////////

func Test_Nil_Nil_FilterSettings(t *testing.T) {

dl, dl_debug := computeDetailLevel(nil, nil, x_qn)
Expand Down
Loading
Loading