From 3a162f74bcff20609493ae19bebf6fe55a063102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Niewrza=C5=82?= Date: Tue, 13 Jan 2026 10:11:06 +0100 Subject: [PATCH 1/2] present: add min_duration filter for trace endpoints Add optional min_duration query parameter to /trace/svg and /trace/json endpoints that filters traces based on their duration. Only traces where the matched span's duration exceeds the specified threshold are returned. Usage examples: /trace/svg?regex=MyFunc&min_duration=2s /trace/json?regex=SlowOp&min_duration=500ms This feature helps in debugging by allowing users to capture only traces that exceed a certain performance threshold, making it easier to identify slow operations. --- present/path.go | 26 ++++++++++++++++++++--- present/trace.go | 55 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/present/path.go b/present/path.go index 29f7198..5321c15 100644 --- a/present/path.go +++ b/present/path.go @@ -26,6 +26,7 @@ import ( "strconv" "strings" "sync" + "time" "github.com/spacemonkeygo/monkit/v3" "github.com/spacemonkeygo/monkit/v3/collect" @@ -73,6 +74,11 @@ func curry(reg *monkit.Registry, // trace id will start a trace until the triggering Span ends, // provided the regex matches. NOTE: the trace_id will be parsed // in hex. +// - min_duration - Optional. If provided, only traces where the matched span +// has a duration >= this value will be returned. Shorter +// traces will be discarded and the wait will continue. +// Format: Go duration string (e.g., "2s", "500ms", "1m30s"). +// Only supported for /trace/svg and /trace/json endpoints. // // By default, regular expressions are matched ahead of time against all known // Funcs, but perhaps the Func you want to trace hasn't been observed by the @@ -193,14 +199,28 @@ func FromRequest(reg *monkit.Registry, path string, query url.Values) ( } } + var minDuration time.Duration + if minDurationStr := query.Get("min_duration"); minDurationStr != "" { + var err error + minDuration, err = time.ParseDuration(minDurationStr) + if err != nil { + return nil, "", errBadRequest.New("invalid min_duration %#v: %v", + minDurationStr, err) + } + if minDuration < 0 { + return nil, "", errBadRequest.New("min_duration must be non-negative: %v", + minDuration) + } + } + switch second { case "svg": return func(w io.Writer) error { - return TraceQuerySVG(reg, w, spanMatcher) + return TraceQuerySVG(reg, w, spanMatcher, minDuration) }, "image/svg+xml; charset=utf-8", nil case "json": return func(w io.Writer) error { - return TraceQueryJSON(reg, w, spanMatcher) + return TraceQueryJSON(reg, w, spanMatcher, minDuration) }, "application/json; charset=utf-8", nil case "remote": viz := query.Get("viz") @@ -316,7 +336,7 @@ func writeIndex(w io.Writer) error {
/trace/json
/trace/svg
-
Trace the next scope that matches one of the ?regex= or ?trace_id= query arguments. By default, regular expressions are matched ahead of time against all known Funcs, but perhaps the Func you want to trace hasn't been observed by the process yet, in which case the regex will fail to match anything. You can turn off this preselection behavior by providing &preselect=false as an additional query param. Be advised that until a trace completes, whether or not it has started, it adds a small amount of overhead (a comparison or two) to every monitored function.
+
Trace the next scope that matches one of the ?regex= or ?trace_id= query arguments. By default, regular expressions are matched ahead of time against all known Funcs, but perhaps the Func you want to trace hasn't been observed by the process yet, in which case the regex will fail to match anything. You can turn off this preselection behavior by providing &preselect=false as an additional query param. Optionally, use &min_duration= (e.g., "2s", "500ms") to filter for traces that exceed a minimum duration. Be advised that until a trace completes, whether or not it has started, it adds a small amount of overhead (a comparison or two) to every monitored function.
`)) diff --git a/present/trace.go b/present/trace.go index dcbc720..a345e3c 100644 --- a/present/trace.go +++ b/present/trace.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "encoding/xml" + "fmt" "io" "strings" "text/template" @@ -391,9 +392,18 @@ func unwrapError(err error) error { // TraceQuerySVG uses WatchForSpans to write all Spans from 'reg' matching // 'matcher' to 'w' in SVG format. func TraceQuerySVG(reg *monkit.Registry, w io.Writer, - matcher func(*monkit.Span) bool) error { - spans, err := watchForSpansWithKeepalive(context.TODO(), - reg, w, matcher, []byte("\n")) + matcher func(*monkit.Span) bool, minDuration time.Duration) error { + var spans []*collect.FinishedSpan + var err error + + if minDuration > 0 { + spans, err = watchForSpansWithMinDuration( + context.TODO(), reg, w, matcher, minDuration, []byte("\n")) + } else { + spans, err = watchForSpansWithKeepalive( + context.TODO(), reg, w, matcher, []byte("\n")) + } + if err != nil { return err } @@ -405,10 +415,19 @@ func TraceQuerySVG(reg *monkit.Registry, w io.Writer, // TraceQueryJSON uses WatchForSpans to write all Spans from 'reg' matching // 'matcher' to 'w' in JSON format. func TraceQueryJSON(reg *monkit.Registry, w io.Writer, - matcher func(*monkit.Span) bool) (write_err error) { + matcher func(*monkit.Span) bool, minDuration time.Duration) (write_err error) { + + var spans []*collect.FinishedSpan + var err error + + if minDuration > 0 { + spans, err = watchForSpansWithMinDuration( + context.TODO(), reg, w, matcher, minDuration, []byte("\n")) + } else { + spans, err = watchForSpansWithKeepalive( + context.TODO(), reg, w, matcher, []byte("\n")) + } - spans, err := watchForSpansWithKeepalive(context.TODO(), - reg, w, matcher, []byte("\n")) if err != nil { return err } @@ -425,6 +444,30 @@ func SpansToJSON(w io.Writer, spans []*collect.FinishedSpan) error { return lw.done() } +func watchForSpansWithMinDuration(ctx context.Context, reg *monkit.Registry, w io.Writer, + matcher func(s *monkit.Span) bool, minDuration time.Duration, keepalive []byte) ( + spans []*collect.FinishedSpan, err error) { + + for { + spans, err = watchForSpansWithKeepalive(ctx, reg, w, matcher, keepalive) + if err != nil { + return nil, err + } + if len(spans) == 0 { + return nil, fmt.Errorf("no spans collected") + } + + // Check root span duration + rootSpan := spans[0] + duration := rootSpan.Finish.Sub(rootSpan.Span.Start()) + if duration >= minDuration { + return spans, nil + } + + // Duration threshold not met, continue watching for another trace + } +} + func watchForSpansWithKeepalive(ctx context.Context, reg *monkit.Registry, w io.Writer, matcher func(s *monkit.Span) bool, keepalive []byte) ( spans []*collect.FinishedSpan, writeErr error) { From 4d82ee9cdb7c3b7411504df8676b5902a47c0387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Niewrza=C5=82?= Date: Tue, 13 Jan 2026 10:19:30 +0100 Subject: [PATCH 2/2] continue if no spans were found --- present/trace.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/present/trace.go b/present/trace.go index a345e3c..4c9aa6d 100644 --- a/present/trace.go +++ b/present/trace.go @@ -18,7 +18,6 @@ import ( "bytes" "context" "encoding/xml" - "fmt" "io" "strings" "text/template" @@ -454,7 +453,7 @@ func watchForSpansWithMinDuration(ctx context.Context, reg *monkit.Registry, w i return nil, err } if len(spans) == 0 { - return nil, fmt.Errorf("no spans collected") + continue } // Check root span duration