Skip to content

Commit 2414489

Browse files
tommaso-moroCopilot
andcommitted
Emit only bytes_full from the fields filter point
Narrow the OSS-side `fields` telemetry to the single metric that can only be measured inside the tool handler: mcp.fields.bytes_full, the size of the unfiltered payload, emitted when a call actually applied filtering and tagged only by tool. Adoption (whether the model requested `fields`) and the filtered response size (bytes_sent) are observable from outside the handler, so they are better emitted by the hosted deployment's request middleware rather than here. This keeps the open-source telemetry surface to one metric while still capturing the counterfactual needed to compute realized savings on the dashboard. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 1f51e0d commit 2414489

6 files changed

Lines changed: 65 additions & 141 deletions

File tree

pkg/github/fields_telemetry.go

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,30 @@ package github
22

33
import (
44
"context"
5-
"strconv"
65
)
76

8-
// Metric names for the optional `fields` response-filtering feature. They let a
9-
// dashboard answer two questions on real traffic: how often the model actually
10-
// filters (adoption) and how many bytes that filtering removes (effectiveness).
7+
// metricFieldsBytesFull is the size, in bytes, of the unfiltered response
8+
// payload for a tool call that applied `fields` filtering. It is emitted only
9+
// when filtering actually happened and is tagged solely by `tool` to keep
10+
// cardinality low.
1111
//
12-
// Cardinality is kept deliberately low: the only tags ever attached are `tool`
13-
// (a small fixed set of tool names) and `filtered` (a boolean). Unbounded values
14-
// such as repository, owner, user, the query, or the requested field list are
15-
// never used as tags.
16-
//
17-
// The realized savings (bytes_full - bytes_sent) is intentionally not emitted as
18-
// its own metric: it is derivable on the dashboard from the two byte counters,
19-
// since sum(bytes_full) - sum(bytes_sent) equals the total saved at any rollup.
20-
const (
21-
metricFieldsToolCall = "mcp.fields.tool_call"
22-
metricFieldsBytesFull = "mcp.fields.bytes_full"
23-
metricFieldsBytesSent = "mcp.fields.bytes_sent"
24-
)
12+
// This is the one `fields` signal that can only be measured inside the tool
13+
// handler: the unfiltered payload exists briefly at the filter point and is gone
14+
// by the time the response leaves the process. The companion signals — adoption
15+
// (whether the model requested `fields`) and the filtered response size
16+
// (bytes_sent) — are observable from outside the handler and are emitted by the
17+
// hosted deployment's request middleware, so they are intentionally not emitted
18+
// here. On the dashboard, bytes_full and bytes_sent combine into the realized
19+
// savings (1 - sum(bytes_sent) / sum(bytes_full)) over filtered calls.
20+
const metricFieldsBytesFull = "mcp.fields.bytes_full"
2521

26-
// recordFieldsUsage emits telemetry for a single call to a tool that supports
27-
// the `fields` parameter. It is best-effort: the local server wires a no-op
28-
// metrics sink, while hosted deployments inject a real sink.
29-
//
30-
// Every call increments mcp.fields.tool_call tagged by tool and whether the
31-
// response was filtered, which yields the adoption rate (filtered / total). When
32-
// the response was filtered, it also records the unfiltered (fullBytes) and
33-
// returned (sentBytes) payload sizes. Byte counters are only emitted for
34-
// filtered calls so that "percent saved" (1 - bytes_sent / bytes_full) is
35-
// computed over the population where filtering actually applied.
36-
func recordFieldsUsage(ctx context.Context, deps ToolDependencies, tool string, filtered bool, fullBytes, sentBytes int) {
22+
// recordFieldsFullBytes emits mcp.fields.bytes_full for a tool call that applied
23+
// `fields` filtering. It is best-effort: the local server wires a no-op metrics
24+
// sink, while hosted deployments inject a real sink.
25+
func recordFieldsFullBytes(ctx context.Context, deps ToolDependencies, tool string, fullBytes int) {
3726
m := deps.Metrics(ctx)
3827
if m == nil {
3928
return
4029
}
41-
42-
m.Increment(metricFieldsToolCall, map[string]string{
43-
"tool": tool,
44-
"filtered": strconv.FormatBool(filtered),
45-
})
46-
47-
if !filtered {
48-
return
49-
}
50-
51-
toolTag := map[string]string{"tool": tool}
52-
m.Counter(metricFieldsBytesFull, toolTag, int64(fullBytes))
53-
m.Counter(metricFieldsBytesSent, toolTag, int64(sentBytes))
30+
m.Counter(metricFieldsBytesFull, map[string]string{"tool": tool}, int64(fullBytes))
5431
}

pkg/github/fields_telemetry_test.go

Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@ import (
1414
)
1515

1616
// recordingMetrics is a metrics.Metrics implementation that captures emitted
17-
// metrics so tests can assert on telemetry. It is safe for concurrent use.
17+
// counters so tests can assert on telemetry. It is safe for concurrent use.
1818
type recordingMetrics struct {
19-
mu sync.Mutex
20-
increments []recordedMetric
21-
counters []recordedMetric
19+
mu sync.Mutex
20+
counters []recordedMetric
2221
}
2322

2423
type recordedMetric struct {
@@ -27,11 +26,7 @@ type recordedMetric struct {
2726
value int64
2827
}
2928

30-
func (m *recordingMetrics) Increment(key string, tags map[string]string) {
31-
m.mu.Lock()
32-
defer m.mu.Unlock()
33-
m.increments = append(m.increments, recordedMetric{key: key, tags: tags, value: 1})
34-
}
29+
func (m *recordingMetrics) Increment(_ string, _ map[string]string) {}
3530

3631
func (m *recordingMetrics) Counter(key string, tags map[string]string, value int64) {
3732
m.mu.Lock()
@@ -55,18 +50,6 @@ func (m *recordingMetrics) counter(key string) (recordedMetric, bool) {
5550
return recordedMetric{}, false
5651
}
5752

58-
// increment returns the recorded increment for the given key, or false if absent.
59-
func (m *recordingMetrics) increment(key string) (recordedMetric, bool) {
60-
m.mu.Lock()
61-
defer m.mu.Unlock()
62-
for _, c := range m.increments {
63-
if c.key == key {
64-
return c, true
65-
}
66-
}
67-
return recordedMetric{}, false
68-
}
69-
7053
// depsWithRecordingMetrics returns BaseDeps wired with a recording metrics sink
7154
// plus the sink for assertions.
7255
func depsWithRecordingMetrics(t *testing.T, base BaseDeps) (BaseDeps, *recordingMetrics) {
@@ -78,46 +61,23 @@ func depsWithRecordingMetrics(t *testing.T, base BaseDeps) (BaseDeps, *recording
7861
return base, rec
7962
}
8063

81-
func Test_recordFieldsUsage_Filtered(t *testing.T) {
64+
func Test_recordFieldsFullBytes(t *testing.T) {
8265
deps, rec := depsWithRecordingMetrics(t, BaseDeps{})
8366

84-
recordFieldsUsage(context.Background(), deps, "search_code", true, 100, 30)
85-
86-
call, ok := rec.increment(metricFieldsToolCall)
87-
require.True(t, ok)
88-
assert.Equal(t, "search_code", call.tags["tool"])
89-
assert.Equal(t, "true", call.tags["filtered"])
67+
recordFieldsFullBytes(context.Background(), deps, "search_code", 100)
9068

9169
full, ok := rec.counter(metricFieldsBytesFull)
9270
require.True(t, ok)
9371
assert.Equal(t, int64(100), full.value)
9472
assert.Equal(t, "search_code", full.tags["tool"])
73+
// The only tag is `tool`; adoption and filtered/unfiltered distinctions are
74+
// emitted by the hosted request middleware, not here.
9575
assert.NotContains(t, full.tags, "filtered")
96-
97-
sent, ok := rec.counter(metricFieldsBytesSent)
98-
require.True(t, ok)
99-
assert.Equal(t, int64(30), sent.value)
100-
}
101-
102-
func Test_recordFieldsUsage_NotFiltered(t *testing.T) {
103-
deps, rec := depsWithRecordingMetrics(t, BaseDeps{})
104-
105-
recordFieldsUsage(context.Background(), deps, "search_code", false, 100, 100)
106-
107-
call, ok := rec.increment(metricFieldsToolCall)
108-
require.True(t, ok)
109-
assert.Equal(t, "false", call.tags["filtered"])
110-
111-
// No byte counters are emitted when the response was not filtered.
112-
_, ok = rec.counter(metricFieldsBytesFull)
113-
assert.False(t, ok)
114-
_, ok = rec.counter(metricFieldsBytesSent)
115-
assert.False(t, ok)
11676
}
11777

118-
func Test_recordFieldsUsage_NilExporterDoesNotPanic(t *testing.T) {
78+
func Test_recordFieldsFullBytes_NilExporterDoesNotPanic(t *testing.T) {
11979
// BaseDeps with no Obsv falls back to a noop sink rather than panicking.
12080
assert.NotPanics(t, func() {
121-
recordFieldsUsage(context.Background(), BaseDeps{}, "search_code", true, 100, 30)
81+
recordFieldsFullBytes(context.Background(), BaseDeps{}, "search_code", 100)
12282
})
12383
}

pkg/github/repositories.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -944,8 +944,8 @@ func getFileContentsTool(t translations.TranslationHelperFunc, includeFields boo
944944
if err != nil {
945945
return utils.NewToolResultError("failed to marshal response"), nil, nil
946946
}
947-
if includeFields {
948-
recordDirContentsFieldsUsage(ctx, deps, dirContent, filtered, len(r))
947+
if filtered {
948+
recordDirContentsFieldsFull(ctx, deps, dirContent)
949949
}
950950
return attachIFC(utils.NewToolResultText(string(r))), nil, nil
951951
}
@@ -955,18 +955,17 @@ func getFileContentsTool(t translations.TranslationHelperFunc, includeFields boo
955955
)
956956
}
957957

958-
// recordDirContentsFieldsUsage emits fields telemetry for a get_file_contents
959-
// directory listing. sentBytes is the size of the payload actually returned.
960-
// When the listing was filtered, the unfiltered size is computed from the full
961-
// directory content so the realized savings can be measured.
962-
func recordDirContentsFieldsUsage(ctx context.Context, deps ToolDependencies, full []*github.RepositoryContent, filtered bool, sentBytes int) {
963-
fullBytes := sentBytes
964-
if filtered {
965-
if data, err := json.Marshal(full); err == nil {
966-
fullBytes = len(data)
967-
}
958+
// recordDirContentsFieldsFull emits mcp.fields.bytes_full for a filtered
959+
// get_file_contents directory listing. The unfiltered size is computed from the
960+
// full directory content so the hosted dashboard can measure realized savings
961+
// against the filtered response size (bytes_sent), which the hosted request
962+
// middleware emits.
963+
func recordDirContentsFieldsFull(ctx context.Context, deps ToolDependencies, full []*github.RepositoryContent) {
964+
data, err := json.Marshal(full)
965+
if err != nil {
966+
return
968967
}
969-
recordFieldsUsage(ctx, deps, "get_file_contents", filtered, fullBytes, sentBytes)
968+
recordFieldsFullBytes(ctx, deps, "get_file_contents", len(data))
970969
}
971970

972971
// ForkRepository creates a tool to fork a repository.

pkg/github/repositories_test.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -601,16 +601,13 @@ func Test_GetFileContents_DirectoryFieldsTelemetry(t *testing.T) {
601601
require.NoError(t, err)
602602
require.False(t, result.IsError)
603603

604-
call, ok := rec.increment(metricFieldsToolCall)
605-
require.True(t, ok)
606-
assert.Equal(t, "get_file_contents", call.tags["tool"])
607-
assert.Equal(t, "true", call.tags["filtered"])
608-
604+
// The unfiltered payload size is emitted from the handler. Adoption and the
605+
// filtered response size (bytes_sent) are emitted by the hosted request
606+
// middleware, so they are not asserted here.
609607
full, ok := rec.counter(metricFieldsBytesFull)
610608
require.True(t, ok)
611-
sent, ok := rec.counter(metricFieldsBytesSent)
612-
require.True(t, ok)
613-
assert.Greater(t, full.value, sent.value, "filtering should remove bytes")
609+
assert.Equal(t, "get_file_contents", full.tags["tool"])
610+
assert.Greater(t, full.value, int64(0))
614611
}
615612

616613
func Test_GetFileContents_IFC_InsidersMode(t *testing.T) {

pkg/github/search.go

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -360,8 +360,8 @@ func searchCodeTool(t translations.TranslationHelperFunc, includeFields bool) in
360360
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
361361
}
362362

363-
if includeFields {
364-
recordSearchCodeFieldsUsage(ctx, deps, minimalResult, filtered, len(r))
363+
if filtered {
364+
recordSearchCodeFieldsFull(ctx, deps, minimalResult)
365365
}
366366

367367
callResult := utils.NewToolResultText(string(r))
@@ -380,18 +380,16 @@ func searchCodeTool(t translations.TranslationHelperFunc, includeFields bool) in
380380
)
381381
}
382382

383-
// recordSearchCodeFieldsUsage emits fields telemetry for a search_code call.
384-
// sentBytes is the size of the payload actually returned. When the response was
385-
// filtered, the unfiltered size is computed from the full minimal result so the
386-
// realized savings can be measured.
387-
func recordSearchCodeFieldsUsage(ctx context.Context, deps ToolDependencies, full *MinimalCodeSearchResult, filtered bool, sentBytes int) {
388-
fullBytes := sentBytes
389-
if filtered {
390-
if data, err := json.Marshal(full); err == nil {
391-
fullBytes = len(data)
392-
}
383+
// recordSearchCodeFieldsFull emits mcp.fields.bytes_full for a filtered
384+
// search_code call. The unfiltered size is computed from the full minimal result
385+
// so the hosted dashboard can measure realized savings against the filtered
386+
// response size (bytes_sent), which the hosted request middleware emits.
387+
func recordSearchCodeFieldsFull(ctx context.Context, deps ToolDependencies, full *MinimalCodeSearchResult) {
388+
data, err := json.Marshal(full)
389+
if err != nil {
390+
return
393391
}
394-
recordFieldsUsage(ctx, deps, "search_code", filtered, fullBytes, sentBytes)
392+
recordFieldsFullBytes(ctx, deps, "search_code", len(data))
395393
}
396394

397395
func userOrOrgHandler(ctx context.Context, accountType string, deps ToolDependencies, args map[string]any) (*mcp.CallToolResult, any, error) {

pkg/github/search_test.go

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ func Test_SearchCode_FieldsTelemetry(t *testing.T) {
610610
GetSearchCode: mockResponse(t, http.StatusOK, mockSearchResult),
611611
}))
612612

613-
t.Run("filtered call records savings", func(t *testing.T) {
613+
t.Run("filtered call records full bytes", func(t *testing.T) {
614614
deps, rec := depsWithRecordingMetrics(t, BaseDeps{Client: client})
615615
handler := serverTool.Handler(deps)
616616

@@ -622,19 +622,16 @@ func Test_SearchCode_FieldsTelemetry(t *testing.T) {
622622
require.NoError(t, err)
623623
require.False(t, result.IsError)
624624

625-
call, ok := rec.increment(metricFieldsToolCall)
626-
require.True(t, ok)
627-
assert.Equal(t, "search_code", call.tags["tool"])
628-
assert.Equal(t, "true", call.tags["filtered"])
629-
625+
// The unfiltered payload size is emitted from the handler. Adoption and
626+
// the filtered response size (bytes_sent) are emitted by the hosted
627+
// request middleware, so they are not asserted here.
630628
full, ok := rec.counter(metricFieldsBytesFull)
631629
require.True(t, ok)
632-
sent, ok := rec.counter(metricFieldsBytesSent)
633-
require.True(t, ok)
634-
assert.Greater(t, full.value, sent.value, "filtering should remove bytes")
630+
assert.Equal(t, "search_code", full.tags["tool"])
631+
assert.Greater(t, full.value, int64(0))
635632
})
636633

637-
t.Run("unfiltered call records adoption only", func(t *testing.T) {
634+
t.Run("unfiltered call records nothing", func(t *testing.T) {
638635
deps, rec := depsWithRecordingMetrics(t, BaseDeps{Client: client})
639636
handler := serverTool.Handler(deps)
640637

@@ -645,12 +642,8 @@ func Test_SearchCode_FieldsTelemetry(t *testing.T) {
645642
require.NoError(t, err)
646643
require.False(t, result.IsError)
647644

648-
call, ok := rec.increment(metricFieldsToolCall)
649-
require.True(t, ok)
650-
assert.Equal(t, "false", call.tags["filtered"])
651-
652-
_, ok = rec.counter(metricFieldsBytesFull)
653-
assert.False(t, ok, "no byte counters when not filtered")
645+
_, ok := rec.counter(metricFieldsBytesFull)
646+
assert.False(t, ok, "no bytes_full when not filtered")
654647
})
655648
}
656649

0 commit comments

Comments
 (0)