Files
Enterprise-Knowledge-Base/internal/tracing/langfuse/context.go
T
wizardchen beca2b89a3 feat(observability): extend Langfuse tracing across asynq pipeline
Previously the Langfuse integration only traced in-process HTTP requests
(chat / search / eval), so file uploads and every downstream asynq task
(document parse, chunk embedding, OCR/VLM, summary / question gen, wiki
ingest, datasource sync, etc.) produced either disconnected shallow
traces or no observation at all.

This change threads one trace end-to-end:

- tracer: add SPAN observation type and StartSpan; add ResumeTrace so a
  worker can attach to an upstream trace without emitting a duplicate
  trace-create; StartGeneration now auto-picks parentObservationId from
  ctx so nested trace -> span -> generation trees render correctly.
- types.TracingContext + LangfuseTracingCarrier: embed on all 17 asynq
  payloads so trace_id / parent_obs_id / user_id / session_id serialise
  into every job.
- langfuse.InjectTracing: injected at 28 enqueue sites before json.Marshal
  so the HTTP-layer trace survives the Redis hop.
- langfuse.AsynqMiddleware: mux.Use hook that peeks the payload, either
  resumes the upstream trace or opens a standalone asynq.<type> trace
  for scheduled jobs, and wraps the handler in a SPAN with task metadata
  (id / queue / retry / payload_bytes) plus ERROR level on failure.
- GinMiddleware.shouldTrace: whitelist ingestion / knowledge-mutation /
  FAQ / wiki / datasource endpoints so the root trace actually starts.
- Tests: tracer_test.go covers span nesting, error status, and
  ResumeTrace no-trace-create guarantee; asynq_test.go covers
  InjectTracing round-trip, middleware resume path, and standalone
  trace fallback.
- Docs: docs/Langfuse\u96c6\u6210.md now lists the covered task types
  and documents the cross-process propagation model.

No behavioural change when Langfuse is disabled (all new code paths are
no-ops and carriers serialise to empty strings with omitempty).
2026-04-24 13:16:47 +08:00

68 lines
2.3 KiB
Go

package langfuse
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// traceCtxKey is the exported context key defined in types/const.go. It lives
// there (not inside this package) so that logger.CloneContext — which rebuilds
// a stripped-down context on every request — can preserve the Langfuse trace
// without importing this package. If we kept the key private here, every
// CloneContext call would drop the trace and downstream LLM wrappers would
// each auto-create their own shallow trace, fragmenting a single HTTP request
// into many unrelated traces in the Langfuse UI.
var traceCtxKey = types.LangfuseTraceContextKey
// parentObsCtxKey tracks the current "parent observation" id — i.e. the span
// that encloses any generation/span started under it. Unlike traceCtxKey, this
// key is private: spans are a pure tracer concern and logger.CloneContext
// should reset the parent across request boundaries (we don't want a leftover
// span id from one request leaking into another's LLM calls).
type parentObsCtxKeyType struct{}
var parentObsCtxKey = parentObsCtxKeyType{}
// withTrace stores a *Trace on the context so downstream LLM wrappers can
// attach their generations to it.
func withTrace(ctx context.Context, t *Trace) context.Context {
if t == nil || ctx == nil {
return ctx
}
return context.WithValue(ctx, traceCtxKey, t)
}
// traceFromCtx retrieves the active trace, if any.
func traceFromCtx(ctx context.Context) (*Trace, bool) {
if ctx == nil {
return nil, false
}
t, ok := ctx.Value(traceCtxKey).(*Trace)
return t, ok && t != nil
}
// TraceFromContext is the public accessor used by HTTP middlewares and
// handlers that want to set the trace input/output on the active trace.
func TraceFromContext(ctx context.Context) (*Trace, bool) {
return traceFromCtx(ctx)
}
// withParentObservation stores the id of the enclosing span so children
// (sub-spans or generations) can attach via parentObservationId.
func withParentObservation(ctx context.Context, id string) context.Context {
if ctx == nil || id == "" {
return ctx
}
return context.WithValue(ctx, parentObsCtxKey, id)
}
// parentObservationFromCtx returns the enclosing span id if any.
func parentObservationFromCtx(ctx context.Context) (string, bool) {
if ctx == nil {
return "", false
}
v, ok := ctx.Value(parentObsCtxKey).(string)
return v, ok && v != ""
}