Upcoming Webinar:

Getting Started with OpenObserve

July 16, 2026
11:00 AM ET

OpenTelemetry for Go: Traces, Metrics & Logs with OpenObserve

Instrument Go apps with the OpenTelemetry SDK — traces, metrics, and logs via OTLP to OpenObserve. Complete setup, code examples, and troubleshooting.

OpenTelemetry guide

Go is a first-class citizen in the OpenTelemetry project: the tracing and metrics SDKs are stable, the instrumentation ecosystem covers the standard library and most popular packages, and the explicit, code-first setup style fits how Go programs are usually built. This guide walks through instrumenting a Go service for traces, metrics, and logs, and shipping all three to OpenObserve over OTLP.

If you are new to OpenTelemetry itself — the signals, the collector, OTLP — start with What is OpenTelemetry? A Complete Guide and come back here for the Go specifics.

Why OpenTelemetry for Go

Go services tend to sit in the hot path — API gateways, backends, workers — where you need request-level visibility without a heavyweight agent. OpenTelemetry gives you:

  • Vendor-neutral instrumentation. Instrument once, export anywhere OTLP is accepted. No proprietary SDK lock-in.
  • A stable, idiomatic API. Context propagation rides on context.Context, which Go code already threads everywhere.
  • A mature library ecosystem. otelhttp, otelgrpc, otelsql, and dozens of other instrumentation libraries in the opentelemetry-go-contrib repository wrap common dependencies with a line or two of code.

Signal stability in Go

Be precise about what “supported” means. As of mid-2026, the status of the Go implementation is:

SignalStatusNotes
TracesStableAPI and SDK stable since 2021; safe for production
MetricsStableAPI and SDK stable; OTLP metric exporters are GA
LogsBetaotel/log API, sdk/log, and bridges such as otelslog work but may see breaking changes between minor releases

Traces and metrics are covered by the project’s stability guarantees. The logs signal is usable in production — many teams run it — but pin your versions and read the changelog when you upgrade, because the API surface is not yet frozen.

Prerequisites

  • Go 1.25 or later. The OpenTelemetry Go SDK supports the two most recent Go releases; older toolchains fall out of support quickly.
  • An OpenObserve instance. Either an OpenObserve Cloud account or a self-hosted instance. For local testing, a single container is enough:
docker run -d --name openobserve \
  -p 5080:5080 -p 5081:5081 \
  -e ZO_ROOT_USER_EMAIL="root@example.com" \
  -e ZO_ROOT_USER_PASSWORD="Complexpass#123" \
  public.ecr.aws/zinclabs/openobserve:latest
  • Your OpenObserve credentials. OTLP ingestion uses HTTP Basic auth. Generate the token once:
echo -n 'root@example.com:Complexpass#123' | base64

This prints cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM= — the value you will pass in the Authorization: Basic header.

In OpenObserve Cloud, the ingestion page under Data Sources shows your organization-specific endpoint and a ready-made Basic auth token, so you rarely need to construct it by hand.

Zero-code instrumentation: the short answer for Go

Go has no runtime agent. A Go program is a statically compiled binary — there is no JVM-style bytecode manipulation or Python-style monkey patching to hook into. If you are coming from Java or .NET, this is the biggest mental shift: in Go, you wire up the SDK in main().

Two things exist that are adjacent to zero-code, and it is worth knowing their real status:

  • OpenTelemetry eBPF Instrumentation (OBI) — the successor to the earlier opentelemetry-go-instrumentation eBPF project — can capture HTTP and gRPC spans from unmodified binaries at the kernel level. It is Linux-only (kernel 5.8+, amd64/arm64) and still pre-1.0, with breaking changes expected between minor releases. Useful for coarse visibility into services you cannot modify; not a replacement for SDK instrumentation.
  • Instrumentation libraries such as otelhttp are as close to “automatic” as idiomatic Go gets: wrap your handler or client once and every request gets a span, without touching handler logic.

The rest of this guide covers the recommended, production-proven path: explicit SDK setup plus instrumentation libraries. It works with any framework — Gin, Echo, Chi, Fiber — because they all sit on net/http or expose middleware hooks, but the examples below use the plain standard library so nothing is framework-specific.

Manual instrumentation

Install the core modules:

go get go.opentelemetry.io/otel \
  go.opentelemetry.io/otel/sdk \
  go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \
  go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp \
  go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp \
  go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp \
  go.opentelemetry.io/contrib/bridges/otelslog

Traces

The pattern is always the same: create an exporter, attach it to a TracerProvider with a batch span processor and a Resource, register the provider globally, and shut it down cleanly on exit. Here is a complete, runnable HTTP service:

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"

	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
)

var tracer = otel.Tracer("checkout-service")

func initTracerProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {
	// Reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS
	// from the environment — see the OpenObserve section below.
	exporter, err := otlptracehttp.New(ctx)
	if err != nil {
		return nil, fmt.Errorf("create trace exporter: %w", err)
	}

	// Merge with resource.Default() so OTEL_SERVICE_NAME and
	// OTEL_RESOURCE_ATTRIBUTES are honored. service.name and
	// deployment.environment come from the environment; service.version
	// is fixed in code here.
	res, err := resource.Merge(
		resource.Default(),
		resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceVersion("1.4.2"),
		),
	)
	if err != nil {
		return nil, fmt.Errorf("create resource: %w", err)
	}

	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
	)
	otel.SetTracerProvider(tp)

	// W3C Trace Context + Baggage propagation for distributed traces.
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
		propagation.TraceContext{},
		propagation.Baggage{},
	))
	return tp, nil
}

func checkoutHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context() // otelhttp already started a server span here

	// Child span for a unit of work inside the request.
	ctx, span := tracer.Start(ctx, "process-payment")
	defer span.End()

	span.SetAttributes(
		attribute.String("payment.provider", "stripe"),
		attribute.Int("cart.items", 3),
	)

	time.Sleep(40 * time.Millisecond) // simulate work
	fmt.Fprintln(w, "order confirmed")
	_ = ctx
}

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	tp, err := initTracerProvider(ctx)
	if err != nil {
		log.Fatal(err)
	}
	// Flush remaining spans before exit — skipping this drops the last batch.
	defer func() {
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := tp.Shutdown(shutdownCtx); err != nil {
			log.Printf("tracer shutdown: %v", err)
		}
	}()

	// otelhttp wraps the handler: one server span per request, automatic
	// http.* semantic-convention attributes, and context extraction from
	// incoming traceparent headers.
	handler := otelhttp.NewHandler(http.HandlerFunc(checkoutHandler), "POST /checkout")
	http.Handle("/checkout", handler)

	srv := &http.Server{Addr: ":8080"}
	go func() { log.Println(srv.ListenAndServe()) }()

	<-ctx.Done()
	srv.Shutdown(context.Background())
}

Two details matter more than anything else in this file:

  1. Context flows through context.Context. tracer.Start(ctx, ...) parents the new span to whatever span lives in ctx. If you drop the context — or start goroutines with context.Background() — you break the trace.
  2. tp.Shutdown is not optional. The batch processor holds spans in memory; a process that exits without shutting down the provider silently loses its final batch.

For outgoing calls, wrap your HTTP client the same way so the traceparent header propagates downstream:

client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://inventory:8081/stock", nil)
resp, err := client.Do(req)

For a deeper walk-through of tracing a multi-service Go system — including gRPC propagation — see Distributed Tracing in Go with OpenTelemetry.

Metrics

Metrics use the same provider pattern, with a periodic reader instead of a batch processor. The reader collects and exports on an interval (60 s by default; 15–30 s is a common production choice):

package main

import (
	"context"
	"fmt"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
	"go.opentelemetry.io/otel/metric"
	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
)

func initMeterProvider(ctx context.Context) (*sdkmetric.MeterProvider, error) {
	exporter, err := otlpmetrichttp.New(ctx)
	if err != nil {
		return nil, fmt.Errorf("create metric exporter: %w", err)
	}

	res, err := resource.Merge(
		resource.Default(),
		resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceVersion("1.4.2"),
		),
	)
	if err != nil {
		return nil, fmt.Errorf("create resource: %w", err)
	}

	mp := sdkmetric.NewMeterProvider(
		sdkmetric.WithResource(res),
		sdkmetric.WithReader(sdkmetric.NewPeriodicReader(
			exporter,
			sdkmetric.WithInterval(15*time.Second),
		)),
	)
	otel.SetMeterProvider(mp)
	return mp, nil
}

func main() {
	ctx := context.Background()
	mp, err := initMeterProvider(ctx)
	if err != nil {
		panic(err)
	}
	defer mp.Shutdown(context.Background())

	meter := otel.Meter("checkout-service")

	orders, _ := meter.Int64Counter("orders.processed",
		metric.WithDescription("Number of orders processed"),
		metric.WithUnit("{order}"),
	)
	latency, _ := meter.Float64Histogram("payment.duration",
		metric.WithDescription("Payment processing duration"),
		metric.WithUnit("ms"),
	)

	// Record from anywhere in your request path:
	start := time.Now()
	// ... process a payment ...
	orders.Add(ctx, 1, metric.WithAttributes(attribute.String("payment.provider", "stripe")))
	latency.Record(ctx, float64(time.Since(start).Milliseconds()))

	time.Sleep(20 * time.Second) // let one export cycle run in this demo
}

Instruments are cheap to create but should be created once (package-level or in a struct), not per request. If you use otelhttp from the traces section, you also get standard HTTP server metrics — request duration and counts — without writing any of this by hand: it records them through the global MeterProvider automatically once one is registered.

Logs

The logs signal in Go is beta. It is designed as a bridge: you keep logging with a normal Go logging API — ideally log/slog from the standard library — and the otelslog bridge forwards records to the OpenTelemetry logs pipeline, which exports them over OTLP. The payoff is that every log written with a request context automatically carries the active trace_id and span_id, so you can pivot from a slow trace to its exact log lines in OpenObserve.

package main

import (
	"context"
	"fmt"

	"go.opentelemetry.io/contrib/bridges/otelslog"
	"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
	"go.opentelemetry.io/otel/log/global"
	sdklog "go.opentelemetry.io/otel/sdk/log"
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
)

func initLoggerProvider(ctx context.Context) (*sdklog.LoggerProvider, error) {
	exporter, err := otlploghttp.New(ctx)
	if err != nil {
		return nil, fmt.Errorf("create log exporter: %w", err)
	}

	res, err := resource.Merge(
		resource.Default(),
		resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceVersion("1.4.2"),
		),
	)
	if err != nil {
		return nil, fmt.Errorf("create resource: %w", err)
	}

	lp := sdklog.NewLoggerProvider(
		sdklog.WithResource(res),
		sdklog.WithProcessor(sdklog.NewBatchProcessor(exporter)),
	)
	global.SetLoggerProvider(lp)
	return lp, nil
}

func main() {
	ctx := context.Background()
	lp, err := initLoggerProvider(ctx)
	if err != nil {
		panic(err)
	}
	defer lp.Shutdown(context.Background())

	// A drop-in *slog.Logger backed by the OTel pipeline.
	logger := otelslog.NewLogger("checkout-service")

	// Pass the request context so trace/span IDs are attached automatically.
	logger.InfoContext(ctx, "order processed",
		"order_id", "ord_8412",
		"amount_cents", 4599,
	)
	logger.ErrorContext(ctx, "payment declined", "reason", "insufficient_funds")
}

Always use the ...Context variants (InfoContext, ErrorContext) inside request handlers — that is how the bridge correlates a log record with the span active in that context. Because the signal is beta, pin go.opentelemetry.io/otel/sdk/log and go.opentelemetry.io/contrib/bridges/otelslog in go.mod and check the changelog before bumping versions. If you prefer to defer adoption, a pragmatic interim setup is: traces and metrics via the SDK, plus JSON logs to stdout collected by an OpenTelemetry Collector or FluentBit and forwarded to OpenObserve.

Sending data to OpenObserve

OpenObserve ingests OTLP natively — no collector required in between, although running one is a fine architecture too. Two transports are supported:

  • OTLP/HTTP at <host>/api/<org>/v1/traces, /v1/metrics, and /v1/logs (port 5080 self-hosted). This is what the otlptracehttp / otlpmetrichttp / otlploghttp exporters above speak.
  • OTLP/gRPC on port 5081 (self-hosted), which additionally requires an organization header alongside Authorization.

Stick with HTTP unless you have a reason not to; it needs the least configuration.

The exporters in the code above take no hardcoded endpoint on purpose — configuration comes from standard environment variables, so the same binary runs against local, staging, and cloud backends.

OpenObserve Cloud:

export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.openobserve.ai/api/<your-org-name>"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic%20<your-base64-token>"
export OTEL_SERVICE_NAME="checkout-service"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production"

go run .

Self-hosted OpenObserve:

export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:5080/api/default"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic%20cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM="
export OTEL_SERVICE_NAME="checkout-service"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=dev"

go run .

Three rules that prevent 90% of setup failures:

  1. No trailing slash on the endpoint. The exporter appends /v1/traces (or /v1/metrics, /v1/logs) itself. .../api/default/ produces .../api/default//v1/traces and a 404.
  2. Percent-encode the space in the header value. OTEL_EXPORTER_OTLP_HEADERS uses a key=value,key=value format in which the value must be URL-encoded — hence Basic%20<token>, not Basic <token>.
  3. The organization is part of the URL path. /api/default targets the default organization; replace it with your org name in OpenObserve Cloud (visible on the ingestion/Data Sources page, which also shows the exact endpoint and token to copy).

If you would rather configure in code than via the environment — common for CLIs or when headers come from a secret manager — every exporter accepts options:

exporter, err := otlptracehttp.New(ctx,
	otlptracehttp.WithEndpointURL("http://localhost:5080/api/default/v1/traces"),
	otlptracehttp.WithHeaders(map[string]string{
		"Authorization": "Basic cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM=",
	}),
)

Note that WithEndpointURL takes the full signal path, whereas the environment variable takes the base URL. Environment variables win in fewer surprises; prefer them. For a broader tour of exporter configuration, see A Beginner’s Guide to OTLP Exporters.

Verifying data arrival in OpenObserve

Run the service, send a few requests (curl http://localhost:8080/checkout), then open the OpenObserve UI (http://localhost:5080 self-hosted, or your cloud URL):

  1. Streams — you should see a default traces stream plus streams for your metrics and logs. If a stream exists, ingestion is working end to end.
  2. Traces — pick your service (checkout-service) in the Traces page, and open a trace. You should see the POST /checkout server span from otelhttp with your process-payment child span nested under it, along with the attributes you set.
  3. Logs — query the log stream and confirm records include trace_id and span_id fields; that confirms the otelslog bridge is correlating correctly.
  4. Metrics — your orders.processed and payment.duration instruments appear as metric streams you can chart or attach to dashboards and alerts.

If nothing appears within ~30 seconds, jump to Troubleshooting below — and check your process’s stderr first, because the Go SDK reports export failures there.

Production tips

  • Keep the batch processor (never SimpleSpanProcessor in production). WithBatcher batches and compresses exports off the hot path. The defaults — 5 s schedule delay, 512-span batches, 2048-span queue — are sensible; tune OTEL_BSP_* variables only if you see queue-full drops.

  • Sample at the head, keep parents authoritative. For high-traffic services, ratio sampling with parent-based override is the standard starting point. Unlike some other language SDKs, the Go SDK does not read OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG — configure the sampler in code when you build the TracerProvider:

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))),
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(res),
    )

    ParentBased(...) matters: it ensures a downstream Go service honors the sampling decision already made upstream, so you get complete traces instead of fragments. Tune the ratio (0.1 keeps 10% of root traces) to match your traffic.

  • Invest in resource attributes. service.name, service.version, and deployment.environment are the dimensions you will filter by in every OpenObserve query. Because the resource is built with resource.Merge(resource.Default(), ...), service.name and deployment.environment are picked up from OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES (so operations can override them per environment without a rebuild), while service.version is set in code.

  • Propagate context into goroutines deliberately. go func() { ... }() with a fresh context.Background() orphans any spans started inside. Capture the request context (or trace.SpanContext) explicitly when fanning out work, and remember the request context is canceled when the handler returns — derive with context.WithoutCancel(ctx) (Go 1.21+) for fire-and-forget work that should still be trace-linked.

  • Shut down every provider on exit. Tracer, meter, and logger providers each buffer data. Wire all three Shutdown calls into your signal-handling path with a timeout, as in the trace example.

  • Record errors on spans, not just in logs. span.RecordError(err) plus span.SetStatus(codes.Error, err.Error()) is what makes failed requests filterable in the Traces UI.

  • Pin and upgrade otel modules together. The API, SDK, exporters, and contrib modules are versioned in lockstep; mixing releases is the most common cause of confusing compile errors after an upgrade. go get go.opentelemetry.io/otel@latest alone is not enough — update the exporters and contrib packages in the same commit.

Troubleshooting

Symptom: exporter logs 404 Not Found and no data arrives. Fix: almost always a URL problem. Remove any trailing slash from OTEL_EXPORTER_OTLP_ENDPOINT, and confirm the path includes your organization (/api/default, not just the host). The final URL the exporter calls should look like http://localhost:5080/api/default/v1/traces.

Symptom: 401 Unauthorized on every export. Fix: regenerate the token with echo -n 'email:password' | base64 (the -n matters — a trailing newline corrupts the token), and make sure the header value in OTEL_EXPORTER_OTLP_HEADERS is Authorization=Basic%20<token> with the space percent-encoded. If you set headers in code via WithHeaders, use a literal space there instead — the encoding rule applies only to the environment variable.

Symptom: traces appear but each service shows disconnected fragments instead of one distributed trace. Fix: context propagation is broken. Confirm otel.SetTextMapPropagator is called with propagation.TraceContext{} in every service, outgoing clients use otelhttp.NewTransport, and requests are created with http.NewRequestWithContext(ctx, ...) — a request built without the context sends no traceparent header.

Symptom: the service exits cleanly but the last few spans/logs never arrive. Fix: the batch processors were never flushed. Call Shutdown (or ForceFlush) on the tracer, meter, and logger providers before exit, with a context timeout so a dead backend cannot hang your shutdown. This bites short-lived jobs and CLIs hardest — for those, consider ForceFlush after each unit of work.

Symptom: spans created inside goroutines are missing or parented to the wrong trace. Fix: the goroutine started from context.Background(). Pass the request-scoped ctx into the goroutine; if the work outlives the request, wrap it with context.WithoutCancel(ctx) so cancellation doesn’t kill the span mid-flight.

Symptom: compile errors like mismatched types across otel packages after go get -u. Fix: partially upgraded modules. Update go.opentelemetry.io/otel, the sdk, all exporters/otlp/... modules, and go.opentelemetry.io/contrib/... packages together, then run go mod tidy. Check each module’s release notes — the log-signal packages in particular can change between minor versions while in beta.

With the SDK wired up once in main(), instrumentation libraries handling the boilerplate, and OTLP pointed at OpenObserve, a Go service gets correlated traces, metrics, and logs in a single backend — searchable side by side, with no vendor-specific code anywhere in your application.

Frequently asked questions

Does Go have automatic (zero-code) OpenTelemetry instrumentation like Java?

No. Go compiles to a static binary, so there is no runtime agent you can attach the way you can with the Java or Python agents. The standard approach is explicit SDK setup in code plus instrumentation libraries such as otelhttp for HTTP servers and clients. An eBPF-based zero-code project (OpenTelemetry eBPF Instrumentation) exists, but it is Linux-only and still pre-1.0, so it is not a substitute for SDK instrumentation in production today.

Which OpenTelemetry signals are stable in Go?

Traces and metrics are stable in the Go SDK. The logs signal (the otel/log API, sdk/log, and bridges like otelslog) is still in beta, meaning the API can change between minor releases. Logs work well in practice, but pin your module versions and read release notes when upgrading.

What endpoint do I use to send OTLP data from Go to OpenObserve?

Set OTEL_EXPORTER_OTLP_ENDPOINT to your OpenObserve base URL including the organization path, with no trailing slash. For OpenObserve Cloud that is https://api.openobserve.ai/api/your-org-name, and for a self-hosted instance it is http://localhost:5080/api/default. The Go OTLP HTTP exporters automatically append /v1/traces, /v1/metrics, and /v1/logs.

How do I authenticate Go OTLP exports to OpenObserve?

OpenObserve uses HTTP Basic authentication. Base64-encode your email and password (or ingestion token) as email:password, then send it as an Authorization header. With environment variables, set OTEL_EXPORTER_OTLP_HEADERS to Authorization=Basic%20<base64-token> — the space after Basic must be percent-encoded as %20. In code, pass the header via the exporter's WithHeaders option instead.

Should I use OTLP over HTTP or gRPC from Go?

Both work with OpenObserve. OTLP/HTTP (port 5080 self-hosted) is simpler to operate — it works through most proxies and load balancers and needs no extra headers beyond Authorization. OTLP/gRPC uses port 5081 on self-hosted OpenObserve and additionally requires an organization header. Start with HTTP unless you have a specific reason to use gRPC.

Why are my Go spans not showing up in OpenObserve?

The most common causes are: the endpoint has a trailing slash (the exporter then builds a wrong URL and gets a 404), the Authorization header space is not encoded as %20 in OTEL_EXPORTER_OTLP_HEADERS, the TracerProvider was never shut down so the last batch was dropped when the process exited, or the organization name in the URL path does not match your OpenObserve organization. Check the exporter's error output first — the Go SDK prints export failures to stderr via the global error handler.

Keep reading

Ship your OpenTelemetry data somewhere better

OpenObserve is a native OTLP backend for logs, metrics, and traces — open source, S3-backed, and a fraction of the cost of legacy observability tools.

Book a DemoBook a Demo