a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
184 lines
6.0 KiB
Go
184 lines
6.0 KiB
Go
package languages
|
|
|
|
import (
|
|
"strings"
|
|
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
)
|
|
|
|
// goLogMethods is the set of method names that, when called as a
|
|
// selector (`slog.Info("…")`, `logger.Error("…")`), are treated as
|
|
// log emissions for the observability coverage domain. The list is
|
|
// intentionally cross-library — slog, zap, zerolog, logrus, log15
|
|
// and most internal wrappers all share the same naming, so the
|
|
// heuristic catches a wide range without per-provider plumbing.
|
|
//
|
|
// False positives are possible (a domain function named Info that
|
|
// takes a string), but in practice the cost is a few extraneous
|
|
// KindEvent nodes — the gate strips them when the user disables
|
|
// observability.
|
|
var goLogMethods = map[string]string{
|
|
"Debug": "log",
|
|
"Info": "log",
|
|
"Warn": "log",
|
|
"Warning": "log",
|
|
"Error": "log",
|
|
"Fatal": "log",
|
|
"Panic": "log",
|
|
"Trace": "log",
|
|
}
|
|
|
|
// goObservabilityEvent is a deferred record of one log call. The
|
|
// event name is the call's first string-literal argument; calls
|
|
// whose first arg is not a literal (e.g. dynamic format strings)
|
|
// are simply not recorded — agents can grep for those if they need
|
|
// to.
|
|
type goObservabilityEvent struct {
|
|
method string // matched log method
|
|
name string // event name, taken from the first string-literal arg
|
|
line int // 1-based line of the call expression
|
|
}
|
|
|
|
// detectGoLogEvent inspects a callm.expr capture and, when it
|
|
// matches the log heuristic, returns the resolved event name and
|
|
// the log-method classification. ok=false when the call is not a
|
|
// log emission.
|
|
//
|
|
// The check is deliberately strict: method must be in goLogMethods,
|
|
// and the first argument must be a string literal. Variadic /
|
|
// formatted overloads (`slog.Info(ctx, msg, ...)`) still match as
|
|
// long as the *first* string-literal argument is found anywhere in
|
|
// the leading arg list — slog passes `msg` as the first string
|
|
// after a context, while package-level slog accepts msg directly.
|
|
func detectGoLogEvent(callExpr *sitter.Node, method string, src []byte) (string, bool) {
|
|
if callExpr == nil {
|
|
return "", false
|
|
}
|
|
if _, ok := goLogMethods[method]; !ok {
|
|
return "", false
|
|
}
|
|
args := callExpr.ChildByFieldName("arguments")
|
|
if args == nil {
|
|
return "", false
|
|
}
|
|
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
|
|
c := args.NamedChild(i)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
if c.Type() != "interpreted_string_literal" && c.Type() != "raw_string_literal" {
|
|
continue
|
|
}
|
|
text := strings.Trim(c.Content(src), "\"`")
|
|
if text == "" {
|
|
return "", false
|
|
}
|
|
return text, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// emitGoObservabilityEvents emits one KindEvent per deferred event
|
|
// plus the EdgeEmits edges from the enclosing function to that
|
|
// event. Event nodes share IDs across files in a repo — the same
|
|
// "user.signup" event name produces a single node that all emitters
|
|
// link to, which is the intended graph topology for the
|
|
// observability domain.
|
|
//
|
|
// callerLookup maps a 1-based line to the enclosing function ID.
|
|
func emitGoObservabilityEvents(events []goObservabilityEvent, callerLookup func(line int) string, filePath string, result *parser.ExtractionResult) {
|
|
if len(events) == 0 {
|
|
return
|
|
}
|
|
seen := make(map[string]struct{}, len(events))
|
|
seenStringNodes := make(map[string]struct{}, len(events))
|
|
seenStringEdges := make(map[string]struct{}, len(events))
|
|
for _, e := range events {
|
|
callerID := callerLookup(e.line)
|
|
if callerID == "" {
|
|
continue
|
|
}
|
|
eventID := goEventNodeID(e.name)
|
|
if _, ok := seen[eventID]; !ok {
|
|
seen[eventID] = struct{}{}
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: eventID,
|
|
Kind: graph.KindEvent,
|
|
Name: e.name,
|
|
FilePath: filePath, // first sighting; not authoritative
|
|
Language: "go",
|
|
Meta: map[string]any{
|
|
"event_kind": "log",
|
|
"name": e.name,
|
|
},
|
|
})
|
|
}
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: callerID,
|
|
To: eventID,
|
|
Kind: graph.EdgeEmits,
|
|
FilePath: filePath,
|
|
Line: e.line,
|
|
Origin: graph.OriginASTInferred,
|
|
Meta: map[string]any{
|
|
"method": e.method,
|
|
},
|
|
})
|
|
// Anchor a KindString context="log_message" registry node so
|
|
// the log_events analyzer can aggregate emitters by raw
|
|
// literal (richer than the canonical KindEvent ID).
|
|
// Meta["level"] is the matched method's severity bucket
|
|
// (every entry in goLogMethods today resolves to "log", but
|
|
// the explicit field future-proofs a per-severity split —
|
|
// Debug→debug, Error→error). Meta["event"] back-links to the
|
|
// KindEvent ID so the registry view and the event view stay
|
|
// round-trippable.
|
|
strID := goStringNodeID(stringCtxLogMessage, e.name)
|
|
if _, dup := seenStringNodes[strID]; !dup {
|
|
seenStringNodes[strID] = struct{}{}
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: strID,
|
|
Kind: graph.KindString,
|
|
Name: e.name,
|
|
FilePath: filePath,
|
|
Language: "go",
|
|
Meta: map[string]any{
|
|
"context": string(stringCtxLogMessage),
|
|
"value": e.name,
|
|
"level": goLogMethods[e.method],
|
|
"event": eventID,
|
|
},
|
|
})
|
|
}
|
|
edgeKey := callerID + "->" + strID
|
|
if _, dup := seenStringEdges[edgeKey]; !dup {
|
|
seenStringEdges[edgeKey] = struct{}{}
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: callerID,
|
|
To: strID,
|
|
Kind: graph.EdgeEmits,
|
|
FilePath: filePath,
|
|
Line: e.line,
|
|
Origin: graph.OriginASTInferred,
|
|
Meta: map[string]any{
|
|
"context": string(stringCtxLogMessage),
|
|
"method": e.method,
|
|
"level": goLogMethods[e.method],
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// goEventNodeID is the canonical ID for a log/metric/trace event
|
|
// node. The "event::" prefix matches the synthetic-ID convention
|
|
// the exporter recognises (alongside `module::`, `external::`,
|
|
// `annotation::`, `unresolved::`), so cross-file events
|
|
// de-duplicate even without a real source-file backing.
|
|
func goEventNodeID(name string) string {
|
|
return "event::log::" + name
|
|
}
|