Go: refactor (#16906)

### Summary

remove and merge code

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
Jin Hai
2026-07-14 19:29:34 +08:00
committed by GitHub
parent a5a6dcfd5d
commit 26161ce844
5 changed files with 21 additions and 712 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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>:<Name>" — 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)

View File

@@ -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 "<Component>:<Name>", 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
}

View File

@@ -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...)
}