diff --git a/internal/common/number.go b/internal/common/number.go index b38260bba8..e1043efc8b 100644 --- a/internal/common/number.go +++ b/internal/common/number.go @@ -150,3 +150,24 @@ func GetInt(value interface{}) (int, bool) { return 0, false } } + +// CoalesceInt returns *val if val is non-nil and positive; otherwise returns +// defaultVal. It is useful for optional int parameters (e.g. pagination) +// where nil or a value <= 0 means "use the default". +func CoalesceInt(val *int, defaultVal int) int { + if val != nil && *val > 0 { + return *val + } + return defaultVal +} + +// IsZeroVector reports whether every element of v is zero. An empty or nil +// slice is considered a zero vector. +func IsZeroVector(v []float64) bool { + for _, x := range v { + if x != 0 { + return false + } + } + return true +} diff --git a/internal/common/numeric.go b/internal/common/numeric.go deleted file mode 100644 index 5ecb244d22..0000000000 --- a/internal/common/numeric.go +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright 2026 The InfiniFlow Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -package common - -// CoalesceInt returns *val if val is non-nil and positive; otherwise returns -// defaultVal. It is useful for optional int parameters (e.g. pagination) -// where nil or a value <= 0 means "use the default". -func CoalesceInt(val *int, defaultVal int) int { - if val != nil && *val > 0 { - return *val - } - return defaultVal -} - -// IsZeroVector reports whether every element of v is zero. An empty or nil -// slice is considered a zero vector. -func IsZeroVector(v []float64) bool { - for _, x := range v { - if x != 0 { - return false - } - } - return true -} diff --git a/internal/observability/otel/handler.go b/internal/observability/otel/handler.go deleted file mode 100644 index 3f1df42671..0000000000 --- a/internal/observability/otel/handler.go +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright 2026 The RAGFlow Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package otel - -import ( - "context" - "sync" - "time" - - "github.com/cloudwego/eino/callbacks" - "github.com/cloudwego/eino/schema" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/embedded" - "go.opentelemetry.io/otel/trace/noop" -) - -// TracerName is the instrumentation scope used for every span created by -// [OtelHandler]. The constant is exported so other components can refer to -// the same scope if they need to look up the tracer. -const TracerName = "github.com/infiniflow/ragflow/internal/observability/otel" - -// Context-key types. These are unexported (the value is exported via the -// constructor helpers below) so that external packages cannot collide -// with the keys we attach to the callback context. -type ( - runIDKeyType struct{} - sessionIDKeyType struct{} - spanContextKeyType struct{} -) - -// WithRunID returns a copy of ctx that carries the supplied canvas run -// id. The handler will read this value and attach it as a "run.id" -// attribute on every emitted span. Pass an empty string to clear. -func WithRunID(ctx context.Context, runID string) context.Context { - return context.WithValue(ctx, runIDKeyType{}, runID) -} - -// RunIDFromContext returns the run id stored on ctx, or "" if none. -func RunIDFromContext(ctx context.Context) string { - if v, ok := ctx.Value(runIDKeyType{}).(string); ok { - return v - } - return "" -} - -// WithSessionID returns a copy of ctx that carries the supplied chat -// session id. The handler will read this value and attach it as a -// "session.id" attribute on every emitted span. -func WithSessionID(ctx context.Context, sessionID string) context.Context { - return context.WithValue(ctx, sessionIDKeyType{}, sessionID) -} - -// SessionIDFromContext returns the session id stored on ctx, or "" if -// none. -func SessionIDFromContext(ctx context.Context) string { - if v, ok := ctx.Value(sessionIDKeyType{}).(string); ok { - return v - } - return "" -} - -var ( - spanContextKey = spanContextKeyType{} - _ = sdktrace.TracerProvider{} // keep sdktrace import meaningful across refactors -) - -// spanContextValue bundles the live span with its start time and any -// stream reader that the streaming callbacks need to clean up. -type spanContextValue struct { - span trace.Span - startTime time.Time - // streamIn is the OnStartWithStreamInput copy the framework handed to - // us; we must close it once we have read (or decided to skip) the - // stream so the framework can recycle the original. - streamIn *schema.StreamReader[callbacks.CallbackInput] - // streamOut is the OnEndWithStreamOutput copy; same cleanup contract. - streamOut *schema.StreamReader[callbacks.CallbackOutput] -} - -// OtelHandler implements [callbacks.Handler] and bridges every eino -// component invocation to an OTel span. -// -// The handler is safe for concurrent use: it derives the per-call span -// from the provider's [trace.Tracer] (which is itself goroutine-safe) and -// stores the in-flight span on the callback context using an unexported -// key. OnEnd / OnError / streaming variants look up that key to finalise -// the span. -// -// A nil *OtelHandler.tp is treated as a no-op: every method returns the -// received context unchanged and creates no span. This makes it cheap to -// install the handler globally in environments that have not configured -// an OTel collector yet. -type OtelHandler struct { - // tp is the provider that owns the tracer used to mint spans. nil - // means "skip everything" — the handler becomes a transparent pass- - // through. - tp *sdktrace.TracerProvider - - // tracer is cached to avoid re-resolving it on every span start. - // It is built lazily from tp so a nil tp does not panic. - tracer trace.Tracer - initOnce sync.Once -} - -// NewOtelHandler wraps tp in a callbacks.Handler. A nil tp is accepted; -// the returned handler then behaves as a pass-through that never emits -// spans, so callers can wire it up unconditionally. -func NewOtelHandler(tp *sdktrace.TracerProvider) *OtelHandler { - return &OtelHandler{tp: tp} -} - -// resolveTracer returns the cached tracer, falling back to the global -// noop tracer when tp is nil. It is the only place that touches tp, so -// the rest of the methods can assume a non-nil tracer. -func (h *OtelHandler) resolveTracer() trace.Tracer { - h.initOnce.Do(func() { - if h.tp == nil { - h.tracer = noop.NewTracerProvider().Tracer(TracerName) - return - } - h.tracer = h.tp.Tracer(TracerName) - }) - return h.tracer -} - -// spanName builds the OTel span name for a given RunInfo. The convention -// is ":" — component category first, then the business -// name (which is the canvas node id for nodes created via -// compose.WithNodeName). When info is nil or both fields are empty the -// span is named just "component" so it is still visible in the trace UI. -func spanName(info *callbacks.RunInfo) string { - if info == nil { - return "component" - } - component := string(info.Component) - name := info.Name - switch { - case component == "" && name == "": - return "component" - case component == "": - return name - case name == "": - return component - default: - return component + ":" + name - } -} - -// runAttributes returns the standard set of attributes that every span -// emitted by the handler carries. The cpn.* / run.id / session.id tuple -// makes the span easy to slice by tenant, canvas or individual run in -// any OTel backend. -func runAttributes(info *callbacks.RunInfo, runID, sessionID string) []attribute.KeyValue { - attrs := []attribute.KeyValue{ - attribute.String("run.id", runID), - attribute.String("session.id", sessionID), - } - if info == nil { - return attrs - } - // Canvas DSL loads each node with cpn_id as the node name, so - // info.Name is a reliable cpn.id surrogate. We expose it under a - // dedicated attribute so dashboards can filter by it directly. - if info.Name != "" { - attrs = append(attrs, - attribute.String("cpn.id", info.Name), - attribute.String("cpn.name", info.Name), - ) - } - if component := string(info.Component); component != "" { - attrs = append(attrs, attribute.String("cpn.component", component)) - } - if info.Type != "" { - attrs = append(attrs, attribute.String("cpn.type", info.Type)) - } - return attrs -} - -// OnStart is the entry point for a non-streaming component invocation. -// It starts a span, attaches the standard run attributes, and stores the -// live span on the returned context so the matching OnEnd/OnError call -// can finalise it. -func (h *OtelHandler) OnStart(ctx context.Context, info *callbacks.RunInfo, _ callbacks.CallbackInput) context.Context { - if h.tp == nil { - return ctx - } - runID := RunIDFromContext(ctx) - sessionID := SessionIDFromContext(ctx) - startedCtx, span := h.resolveTracer().Start(ctx, spanName(info), - trace.WithTimestamp(time.Now()), - trace.WithAttributes(runAttributes(info, runID, sessionID)...), - ) - return context.WithValue(startedCtx, spanContextKey, &spanContextValue{ - span: span, - startTime: time.Now(), - }) -} - -// OnEnd finalises the span started by the matching OnStart. It is a no-op -// if there is no in-flight span on the context (which can happen when -// the handler is installed in a chain alongside another handler that -// consumed the span first — defensive programming for the -// order-independent multi-handler contract). -func (h *OtelHandler) OnEnd(ctx context.Context, info *callbacks.RunInfo, _ callbacks.CallbackOutput) context.Context { - v, ok := ctx.Value(spanContextKey).(*spanContextValue) - if !ok || v == nil { - return ctx - } - // Drop the key so the same ctx is not reused for a different span - // should eino (or a future refactor) ever share callbacks across - // goroutines. - v.span.End(trace.WithTimestamp(time.Now())) - return context.WithValue(ctx, spanContextKey, (*spanContextValue)(nil)) -} - -// OnError records the error on the in-flight span and marks it with -// OTel's "Error" status code. If no span is on the context (e.g. OnStart -// was never called, or the handler is in no-op mode), OnError is a no-op. -func (h *OtelHandler) OnError(ctx context.Context, info *callbacks.RunInfo, err error) context.Context { - if err == nil { - // Treat nil error as a non-event so we never mark a span Error - // when the framework calls us defensively. - return ctx - } - v, ok := ctx.Value(spanContextKey).(*spanContextValue) - if !ok || v == nil { - return ctx - } - v.span.RecordError(err, trace.WithTimestamp(time.Now())) - v.span.SetStatus(codes.Error, err.Error()) - v.span.End(trace.WithTimestamp(time.Now())) - return context.WithValue(ctx, spanContextKey, (*spanContextValue)(nil)) -} - -// OnStartWithStreamInput mirrors [OtelHandler.OnStart] for streaming -// inputs. eino hands us a *schema.StreamReader that we own a copy of; -// we must close it after we are done so the framework can release the -// original. The handler does not consume the stream — that is the -// downstream component's job — so the close is all the bookkeeping we -// need. -func (h *OtelHandler) OnStartWithStreamInput(ctx context.Context, info *callbacks.RunInfo, - input *schema.StreamReader[callbacks.CallbackInput]) context.Context { - if h.tp == nil { - if input != nil { - input.Close() - } - return ctx - } - runID := RunIDFromContext(ctx) - sessionID := SessionIDFromContext(ctx) - startedCtx, span := h.resolveTracer().Start(ctx, spanName(info), - trace.WithTimestamp(time.Now()), - trace.WithAttributes(runAttributes(info, runID, sessionID)...), - trace.WithSpanKind(trace.SpanKindConsumer), - ) - if input != nil { - input.Close() - } - return context.WithValue(startedCtx, spanContextKey, &spanContextValue{ - span: span, - startTime: time.Now(), - streamIn: input, - }) -} - -// OnEndWithStreamOutput mirrors [OtelHandler.OnEnd] for streaming -// outputs. The framework hands us a copy of the output stream; we -// close it without consuming it (the SSE handler in the canvas package -// is the actual consumer) and finalise the span. -func (h *OtelHandler) OnEndWithStreamOutput(ctx context.Context, info *callbacks.RunInfo, - output *schema.StreamReader[callbacks.CallbackOutput]) context.Context { - v, ok := ctx.Value(spanContextKey).(*spanContextValue) - if !ok || v == nil { - if output != nil { - output.Close() - } - return ctx - } - if output != nil { - output.Close() - v.streamOut = output - } - v.span.End(trace.WithTimestamp(time.Now())) - return context.WithValue(ctx, spanContextKey, (*spanContextValue)(nil)) -} - -// Compile-time assertion that *OtelHandler satisfies the eino Handler -// interface. Catches signature drift the moment the file is compiled. -var _ callbacks.Handler = (*OtelHandler)(nil) - -// Keep the embedded interface referenced so an upgrade that drops -// trace/embedded from go.opentelemetry.io/otel still surfaces a -// compile error here rather than a silent Tracer regression. -var _ embedded.Tracer = (embedded.Tracer)(nil) diff --git a/internal/observability/otel/handler_test.go b/internal/observability/otel/handler_test.go deleted file mode 100644 index 4d130c32c9..0000000000 --- a/internal/observability/otel/handler_test.go +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2026 The RAGFlow Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package otel - -import ( - "context" - "errors" - "testing" - - "github.com/cloudwego/eino/callbacks" - "github.com/cloudwego/eino/components" - "go.opentelemetry.io/otel/codes" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/sdk/trace/tracetest" -) - -// newTestHandler returns a *OtelHandler wired to an in-memory -// [tracetest.SpanRecorder]. The recorder exposes the spans the handler -// emits so tests can assert on names, attributes and status without -// needing a real OTel collector. -func newTestHandler(t *testing.T) (*OtelHandler, *tracetest.SpanRecorder) { - t.Helper() - recorder := tracetest.NewSpanRecorder() - tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) - t.Cleanup(func() { - _ = tp.Shutdown(context.Background()) - }) - h := NewOtelHandler(tp) - return h, recorder -} - -// TestOtelHandler_RecordsSpan asserts that a single OnStart/OnEnd pair -// produces exactly one span, named ":", carrying the -// cpn.id / cpn.name / run.id / session.id attributes derived from the -// context and the RunInfo. -func TestOtelHandler_RecordsSpan(t *testing.T) { - h, recorder := newTestHandler(t) - - ctx := context.Background() - ctx = WithRunID(ctx, "run-123") - ctx = WithSessionID(ctx, "sess-456") - - info := &callbacks.RunInfo{ - Name: "llm_1", - Type: "OpenAI", - Component: components.ComponentOfChatModel, - } - - ctx = h.OnStart(ctx, info, "prompt") - ctx = h.OnEnd(ctx, info, "answer") - - spans := recorder.Ended() - if got, want := len(spans), 1; got != want { - t.Fatalf("span count: got %d, want %d", got, want) - } - span := spans[0] - if got, want := span.Name(), "ChatModel:llm_1"; got != want { - t.Errorf("span name: got %q, want %q", got, want) - } - - wantAttrs := map[string]string{ - "cpn.id": "llm_1", - "cpn.name": "llm_1", - "cpn.component": "ChatModel", - "cpn.type": "OpenAI", - "run.id": "run-123", - "session.id": "sess-456", - } - gotAttrs := attrMap(span) - for k, want := range wantAttrs { - if got := gotAttrs[k]; got != want { - t.Errorf("attribute %q: got %q, want %q", k, got, want) - } - } -} - -// TestOtelHandler_RecordsError asserts that OnError attaches the error -// to the span (RecordedErrorCount > 0) and flips the span status to -// OTel's Error code. The check on recorded-error count is portable -// across the small API changes the SDK has shipped. -func TestOtelHandler_RecordsError(t *testing.T) { - h, recorder := newTestHandler(t) - - ctx := context.Background() - info := &callbacks.RunInfo{ - Name: "retrieval_0", - Component: components.ComponentOfRetriever, - } - - ctx = h.OnStart(ctx, info, nil) - ctx = h.OnError(ctx, info, errors.New("kaboom")) - - spans := recorder.Ended() - if got, want := len(spans), 1; got != want { - t.Fatalf("span count: got %d, want %d", got, want) - } - span := spans[0] - - // Two independent assertions: (1) the span status is Error per - // OTel spec, (2) at least one "exception" event was recorded with - // the err.Error() message attached. Either is sufficient to prove - // the OnError path propagated the error to OTel; together they - // guard against regressions in either half of the contract. - if status := span.Status(); status.Code != codes.Error { - t.Errorf("span status code: got %v, want %v", status.Code, codes.Error) - } - foundException := false - for _, ev := range span.Events() { - if ev.Name == "exception" { - for _, kv := range ev.Attributes { - if string(kv.Key) == "exception.message" && kv.Value.AsString() == "kaboom" { - foundException = true - } - } - } - } - if !foundException { - t.Errorf("expected an \"exception\" event with message \"kaboom\"; events: %+v", span.Events()) - } -} - -// TestOtelHandler_NoOpWhenProviderNil asserts that constructing a -// handler with a nil provider is safe: OnStart returns the same context -// (no span attached, no panic), and no spans are emitted to a recorder -// the test later installs. -func TestOtelHandler_NoOpWhenProviderNil(t *testing.T) { - h := NewOtelHandler(nil) - if h == nil { - t.Fatal("NewOtelHandler(nil) returned nil handler") - } - - ctx := context.Background() - info := &callbacks.RunInfo{ - Name: "noop_0", - Component: components.Component("Lambda"), - } - - out := h.OnStart(ctx, info, nil) - if out != ctx { - t.Errorf("OnStart should return ctx unchanged when tp is nil, got %v want %v", out, ctx) - } - out = h.OnEnd(out, info, nil) - if out != ctx { - t.Errorf("OnEnd should return ctx unchanged when tp is nil, got %v want %v", out, ctx) - } - - // OnError with nil tp must also be a clean no-op: no panic, ctx - // unchanged. - out = h.OnError(out, info, errors.New("ignored")) - if out != ctx { - t.Errorf("OnError should return ctx unchanged when tp is nil, got %v want %v", out, ctx) - } - - // And — for completeness — the streaming variants must not panic - // when tp is nil. The framework may pass nil readers; we treat them - // as best-effort and assert no-op behaviour. - _ = h.OnStartWithStreamInput(ctx, info, nil) - _ = h.OnEndWithStreamOutput(ctx, info, nil) -} - -// attrMap flattens a span's attributes into a string map so the test -// can assert on individual key/value pairs without worrying about -// attribute ordering (the SDK does not guarantee it). -func attrMap(s sdktrace.ReadOnlySpan) map[string]string { - out := make(map[string]string, len(s.Attributes())) - for _, kv := range s.Attributes() { - out[string(kv.Key)] = kv.Value.AsString() - } - return out -} diff --git a/internal/observability/otel/provider.go b/internal/observability/otel/provider.go deleted file mode 100644 index c8277e4af3..0000000000 --- a/internal/observability/otel/provider.go +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2026 The RAGFlow Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// Package otel provides OpenTelemetry-based observability for the RAGFlow -// agent canvas runtime. -// -// The package exposes a TracerProvider factory and a callbacks.Handler -// implementation that maps eino graph-node lifecycle events to OTel spans. -// The handler is designed to be a no-op when tracing is not configured, so -// production code can wire it up unconditionally without paying any cost -// in deployments that do not run an OTel collector. -package otel - -import ( - "context" - "fmt" - "time" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" - "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.26.0" -) - -// Default values applied when ProviderConfig fields are left zero. -const ( - defaultServiceName = "ragflow" - defaultServiceVersion = "0.0.0" - defaultServiceNS = "ragflow" - defaultExportTimeout = 30 * time.Second -) - -// ProviderConfig configures the OTel TracerProvider built by -// [NewTracerProvider]. Zero values fall back to sensible defaults that -// keep the provider invisible in the runtime: a no-op BatchSpanProcessor -// is not attached when OTLPEndpoint is empty or SampleRatio is 0. -type ProviderConfig struct { - // ServiceName populates the "service.name" resource attribute. Defaults - // to "ragflow" when empty. - ServiceName string - // ServiceVersion populates the "service.version" resource attribute. - // Defaults to "0.0.0" when empty. - ServiceVersion string - // OTLPEndpoint is the OTLP/HTTP collector endpoint (e.g. - // "http://otel-collector:4318"). When empty, the returned provider - // has no exporter and effectively no-ops. - OTLPEndpoint string - // Insecure disables TLS for the OTLP exporter. Defaults to true. - Insecure bool - // SampleRatio is the probability an in-process trace is sampled, - // in the [0, 1] range. 0 disables the provider (no exporter, no - // sampler wiring). Defaults to 1.0 (sample everything). - SampleRatio float64 -} - -// NewTracerProvider builds a [sdktrace.TracerProvider] honouring cfg. -// -// Two failure modes are special-cased and never return an error: -// -// - cfg.OTLPEndpoint == "": returns a provider with no exporter. Useful -// for unit tests and for deployments that do not yet run a collector. -// - cfg.SampleRatio == 0: returns a provider configured with -// [trace.NeverSample] and no exporter, so even a single manual span -// is dropped. -// -// All other misconfigurations (unparseable ratio, collector unreachability) -// are reported through the returned error. The caller is expected to log -// the error and fall back to a no-op provider so that the agent runtime -// remains operational. -func NewTracerProvider(ctx context.Context, cfg ProviderConfig) (*sdktrace.TracerProvider, error) { - cfg = withDefaults(cfg) - - // Short-circuit: no endpoint or no sampling requested → no-op provider. - // We deliberately still return a non-nil *sdktrace.TracerProvider so - // the handler does not need to special-case nil. - if cfg.OTLPEndpoint == "" || cfg.SampleRatio == 0 { - return sdktrace.NewTracerProvider( - sdktrace.WithSampler(sdktrace.NeverSample()), - ), nil - } - - res, err := buildResource(ctx, cfg) - if err != nil { - return nil, fmt.Errorf("otel: build resource: %w", err) - } - - exporter, err := buildExporter(ctx, cfg) - if err != nil { - return nil, fmt.Errorf("otel: build exporter: %w", err) - } - - bsp := sdktrace.NewBatchSpanProcessor(exporter, - sdktrace.WithExportTimeout(defaultExportTimeout), - ) - - tp := sdktrace.NewTracerProvider( - sdktrace.WithResource(res), - sdktrace.WithSpanProcessor(bsp), - sdktrace.WithSampler(sdktrace.TraceIDRatioBased(cfg.SampleRatio)), - ) - - // Register as the global tracer provider so that any code that calls - // otel.Tracer("...") also routes through this provider. - otel.SetTracerProvider(tp) - - return tp, nil -} - -// withDefaults fills the zero-valued fields of cfg with the package-level -// defaults. The receiver is passed by value, so the caller's config is -// not mutated. -func withDefaults(cfg ProviderConfig) ProviderConfig { - if cfg.ServiceName == "" { - cfg.ServiceName = defaultServiceName - } - if cfg.ServiceVersion == "" { - cfg.ServiceVersion = defaultServiceVersion - } - // SampleRatio is a float — guard against negative values as well, - // treating them as "disabled" to match the explicit-zero behaviour. - if cfg.SampleRatio < 0 { - cfg.SampleRatio = 0 - } - return cfg -} - -// buildResource composes the OTel resource (process identity) attached to -// every span emitted by the provider. The resource uses semconv v1.26.0 -// attribute keys. -func buildResource(ctx context.Context, cfg ProviderConfig) (*resource.Resource, error) { - schemaURL := semconv.SchemaURL - - // service.namespace is set to "ragflow" regardless of cfg so that the - // Go runtime and the Python RAGFlow share a single namespace in any - // shared OTel backend (see plan §2.10.8). - attrs := resource.NewWithAttributes( - schemaURL, - semconv.ServiceName(cfg.ServiceName), - semconv.ServiceVersion(cfg.ServiceVersion), - semconv.ServiceNamespace(defaultServiceNS), - ) - detected, err := resource.Merge( - resource.Default(), - attrs, - ) - if err != nil { - return nil, err - } - return detected, nil -} - -// buildExporter constructs an OTLP/HTTP span exporter pointed at the -// configured collector endpoint. Insecure defaults to true; callers that -// need TLS should set cfg.Insecure=false. -func buildExporter(ctx context.Context, cfg ProviderConfig) (*otlptrace.Exporter, error) { - opts := []otlptracehttp.Option{ - otlptracehttp.WithEndpoint(cfg.OTLPEndpoint), - otlptracehttp.WithTimeout(defaultExportTimeout), - } - if cfg.Insecure { - opts = append(opts, otlptracehttp.WithInsecure()) - } - return otlptracehttp.New(ctx, opts...) -}