Skip to content

Commit 9a493ea

Browse files
authored
refactor: move DIFC filter events into JSONL log (#2077)
Write DIFC_FILTERED entries to rpc-messages.jsonl alongside RPC request/response traffic so filter events appear in context with the tool call that triggered them. - Add JSONLFilteredItem type and LogDifcFilteredItem() to JSONL logger - Extract logEntry() helper from LogMessage() for generic JSONL writes - Remove dedicated DifcFilterEntry type (replaced by JSONLFilteredItem) - No new log files; filter events share the existing JSONL stream
2 parents 490f98f + d4bf94a commit 9a493ea

2 files changed

Lines changed: 72 additions & 19 deletions

File tree

internal/logger/jsonl_logger.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,22 @@ func (jl *JSONLLogger) Close() error {
7777

7878
// LogMessage logs an RPC message to the JSONL file
7979
func (jl *JSONLLogger) LogMessage(entry *JSONLRPCMessage) error {
80+
return jl.logEntry(entry)
81+
}
82+
83+
// logEntry writes any JSON-serializable value as a single JSONL line.
84+
func (jl *JSONLLogger) logEntry(entry interface{}) error {
8085
jl.mu.Lock()
8186
defer jl.mu.Unlock()
8287

8388
if jl.logFile == nil {
8489
return fmt.Errorf("JSONL logger not initialized")
8590
}
8691

87-
// Encode and write the JSON object followed by a newline
8892
if err := jl.encoder.Encode(entry); err != nil {
8993
return fmt.Errorf("failed to encode JSON: %w", err)
9094
}
9195

92-
// Flush to disk immediately
9396
if err := jl.logFile.Sync(); err != nil {
9497
return fmt.Errorf("failed to sync log file: %w", err)
9598
}
@@ -135,3 +138,39 @@ func LogRPCMessageJSONLWithTags(direction RPCMessageDirection, messageType RPCMe
135138
_ = logger.LogMessage(entry)
136139
})
137140
}
141+
142+
// JSONLFilteredItem represents a DIFC-filtered item logged to the JSONL stream.
143+
// These entries appear alongside RPC messages so filter events are visible
144+
// in context with the request/response that triggered them.
145+
type JSONLFilteredItem struct {
146+
Timestamp string `json:"timestamp"`
147+
Type string `json:"type"` // Always "DIFC_FILTERED"
148+
ServerID string `json:"server_id"`
149+
ToolName string `json:"tool_name"`
150+
Description string `json:"description"`
151+
Reason string `json:"reason"`
152+
SecrecyTags []string `json:"secrecy_tags"`
153+
IntegrityTags []string `json:"integrity_tags"`
154+
AuthorAssociation string `json:"author_association,omitempty"`
155+
AuthorLogin string `json:"author_login,omitempty"`
156+
HTMLURL string `json:"html_url,omitempty"`
157+
Number string `json:"number,omitempty"`
158+
SHA string `json:"sha,omitempty"`
159+
}
160+
161+
// LogDifcFilteredItem writes a DIFC filter event to the JSONL log.
162+
func LogDifcFilteredItem(entry *JSONLFilteredItem) {
163+
if entry == nil {
164+
// Best-effort logging: avoid panicking on nil input.
165+
return
166+
}
167+
168+
entry.Timestamp = time.Now().UTC().Format(time.RFC3339Nano)
169+
entry.Type = "DIFC_FILTERED"
170+
withGlobalLogger(&globalJSONLMu, &globalJSONLLogger, func(logger *JSONLLogger) {
171+
if logger == nil {
172+
return
173+
}
174+
_ = logger.logEntry(entry)
175+
})
176+
}

internal/server/difc_log.go

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import (
88
"github.com/github/gh-aw-mcpg/internal/logger"
99
)
1010

11-
// FilteredItemLogEntry is a structured log entry for a DIFC-filtered item.
12-
// It captures enough context to identify the object, understand why it was
13-
// filtered, and link back to it in a post-processing report.
11+
// FilteredItemLogEntry is the structured log record emitted for each item
12+
// removed by DIFC filtering. It is written as JSON to the unified and
13+
// per-server text log files under the [DIFC-FILTERED] marker so that filter
14+
// events can be correlated with the MCP request/response that triggered them.
1415
type FilteredItemLogEntry struct {
1516
ServerID string `json:"server_id"`
1617
ToolName string `json:"tool_name"`
@@ -26,26 +27,24 @@ type FilteredItemLogEntry struct {
2627
}
2728

2829
// logFilteredItems logs structured details for every item removed by DIFC filtering.
29-
// Each item is logged individually to the configured logger outputs so that
30-
// post-processing tools can reconstruct exactly what was filtered and why.
30+
// Each item is written as a [DIFC-FILTERED] JSON entry to both the unified and
31+
// per-server text log files (via LogInfoWithServer), and as a DIFC_FILTERED entry
32+
// in the JSONL log.
3133
func logFilteredItems(serverID, toolName string, filtered *difc.FilteredCollectionLabeledData) {
3234
for _, detail := range filtered.Filtered {
3335
entry := buildFilteredItemLogEntry(serverID, toolName, detail)
34-
35-
entryJSON, err := json.Marshal(entry)
36+
b, err := json.Marshal(entry)
3637
if err != nil {
37-
logger.LogWarnWithServer(serverID, "difc",
38-
"[DIFC-FILTERED] %s | %s | description=%s | reason=%s (json marshal failed: %v)",
39-
serverID, toolName, entry.Description, entry.Reason, err)
38+
logger.LogInfoWithServer(serverID, "difc", "Failed to marshal filtered item log entry: %v", err)
4039
continue
4140
}
42-
43-
logger.LogInfoWithServer(serverID, "difc",
44-
"[DIFC-FILTERED] %s", string(entryJSON))
41+
jsonStr := string(b)
42+
logger.LogInfoWithServer(serverID, "difc", "[DIFC-FILTERED] %s", jsonStr)
43+
logger.LogDifcFilteredItem(entry.toJSONLFilteredItem())
4544
}
4645
}
4746

48-
// buildFilteredItemLogEntry constructs a structured log entry from a filtered item.
47+
// buildFilteredItemLogEntry constructs a FilteredItemLogEntry from a filtered item.
4948
func buildFilteredItemLogEntry(serverID, toolName string, detail difc.FilteredItemDetail) FilteredItemLogEntry {
5049
entry := FilteredItemLogEntry{
5150
ServerID: serverID,
@@ -72,6 +71,24 @@ func buildFilteredItemLogEntry(serverID, toolName string, detail difc.FilteredIt
7271
return entry
7372
}
7473

74+
// toJSONLFilteredItem converts a FilteredItemLogEntry to a logger.JSONLFilteredItem
75+
// for writing to the JSONL log.
76+
func (e FilteredItemLogEntry) toJSONLFilteredItem() *logger.JSONLFilteredItem {
77+
return &logger.JSONLFilteredItem{
78+
ServerID: e.ServerID,
79+
ToolName: e.ToolName,
80+
Description: e.Description,
81+
Reason: e.Reason,
82+
SecrecyTags: e.SecrecyTags,
83+
IntegrityTags: e.IntegrityTags,
84+
AuthorAssociation: e.AuthorAssociation,
85+
AuthorLogin: e.AuthorLogin,
86+
HTMLURL: e.HTMLURL,
87+
Number: e.Number,
88+
SHA: e.SHA,
89+
}
90+
}
91+
7592
// tagsToStrings converts DIFC tags to string slice.
7693
func tagsToStrings(tags []difc.Tag) []string {
7794
s := make([]string, len(tags))
@@ -96,13 +113,11 @@ func getStringField(m map[string]interface{}, fields ...string) string {
96113

97114
// extractAuthorLogin extracts the author login from nested user/author objects.
98115
func extractAuthorLogin(m map[string]interface{}) string {
99-
// Try user.login (issues, PRs)
100116
if user, ok := m["user"].(map[string]interface{}); ok {
101117
if login, ok := user["login"].(string); ok {
102118
return login
103119
}
104120
}
105-
// Try author.login (commits)
106121
if author, ok := m["author"].(map[string]interface{}); ok {
107122
if login, ok := author["login"].(string); ok {
108123
return login
@@ -112,7 +127,6 @@ func extractAuthorLogin(m map[string]interface{}) string {
112127
}
113128

114129
// extractNumberField extracts the item number as a string.
115-
// GitHub API returns numbers as float64 from JSON parsing.
116130
func extractNumberField(m map[string]interface{}) string {
117131
if n, ok := m["number"]; ok {
118132
switch v := n.(type) {

0 commit comments

Comments
 (0)