Overview
otelfile is a developer-experience (DX) focused OpenTelemetry span exporter for Go. It is engineered specifically for active development and manual endpoint testing.
Traditional tracing solutions force you to choose between running zero telemetry or hosting a complete Jaeger, Tempo, or OTel Collector database container. This introduces port constraints and system overhead that feel excessive during active developer loops.
The Concept: Just like logging to a file (e.g.,
app.log), otelfile dumps traces to a
portable, single-file HTML document (trace.html). You
can open this page directly in any modern browser to view full,
interactive distributed tracing timelines.
Installation
Add otelfile to your Go module dependencies. The library
is fully compliant with the standard
go.opentelemetry.io/otel/sdk/trace core interfaces.
go get github.com/mukailasam/otelfile
Quick Start
Initialize the FileExporter and register it with your
application's TracerProvider during setup.
package main
import (
"context"
"github.com/mukailasam/otelfile"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
func main() {
// Create the file-based exporter (Write to "trace.html", keep last 100 traces)
exporter := otelfile.NewFileExporter("trace.html", 100)
tp := sdktrace.NewTracerProvider(
sdktrace.WithSimpleSpanProcessor(exporter),
)
otel.SetTracerProvider(tp)
defer tp.Shutdown(context.Background())
// Instrument as normal!
}
The Local Development Loop
Using otelfile is designed to feel visual and
instantaneous. Once registered in your application, you can test
endpoints as you write them:
1. Run your local application to initiate the export process:
go run main.go
2. Launch the HTML tracker file in your browser using standard operating system utilities:
# macOS
open trace.html
# Linux
xdg-open trace.html
# Windows
start trace.html
3. Trigger endpoints (using Postman, Chrome, or cURL):
curl http://localhost:8080/checkout
4. Refresh the browser tab. The trace list on the left will populate with your new requests. Select any item to view its nested, visual waterfall chart in real-time.
Configuration
The initialization function accepts two simple parameters:
NewFileExporter(filePath string, maxTraces int)
- filePath: The location on disk where the single-file
HTML dashboard will be generated. Recommended:
"trace.html".
- maxTraces: The rolling in-memory threshold. Keeps your
HTML size small by automatically evicting historical spans when the
threshold is crossed.
Basic Example
package main
import (
"context"
"fmt"
"math/rand"
"net/http"
"time"
"github.com/mukailasam/otelfile" // Your local package path
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"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.17.0"
)
// initTracer initializes the OpenTelemetry pipeline using otelfile as the local exporter
func initTracer(serviceName string, filePath string) (*sdktrace.TracerProvider, func(context.Context) error, error) {
if serviceName == "" {
serviceName = "unknown_service"
}
// Create our file-based exporter (keeps a rolling log of 50 traces)
exporter := otelfile.NewFileExporter(filePath, 0)
// Build the resource matching semconv patterns and SchemaURL correctly
res, err := resource.New(context.Background(),
resource.WithSchemaURL(semconv.SchemaURL), // Correctly passes the schema URL
resource.WithAttributes(
semconv.ServiceName(serviceName),
),
)
if err != nil {
return nil, nil, err
}
// Register the exporter with the correct SimpleSpanProcessor setup
tp := sdktrace.NewTracerProvider(
sdktrace.WithSpanProcessor(sdktrace.NewSimpleSpanProcessor(exporter)), // Correct method signature
sdktrace.WithResource(res),
)
// Set global tracer provider
otel.SetTracerProvider(tp)
// Establish Text Map Propagators (critical for context parsing across requests)
otel.SetTextMapPropagator(
propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
),
)
// Return the provider and a safe shutdown function
shutdownFunc := func(ctx context.Context) error {
if tp != nil {
return tp.Shutdown(ctx)
}
return nil
}
return tp, shutdownFunc, nil
}
func main() {
// Initialize otelfile tracing pipeline on payment-api service
_, shutdown, err := initTracer("payment-api", "trace.html")
if err != nil {
panic(err)
}
defer func() {
_ = shutdown(context.Background())
}()
// Grab our tracer
tracer := otel.Tracer("http-server")
// Setup local web server for endpoint manual testing
http.HandleFunc("/checkout", func(w http.ResponseWriter, r *http.Request) {
// Extract incoming trace context from headers (like Postman / browser client headers)
ctx := propagation.TraceContext{}.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
// Start the root span using the extracted context
ctx, span := tracer.Start(ctx, "HTTP POST /checkout")
defer span.End()
span.SetAttributes(
attribute.String("http.method", "POST"),
attribute.String("checkout.cart_id", "cart_abc123"),
)
// Simulate Authorization child span
func() {
_, childSpan := tracer.Start(ctx, "AuthService.Authorize")
defer childSpan.End()
time.Sleep(time.Duration(30+rand.Intn(40)) * time.Millisecond)
}()
// Simulate Database child span
func() {
_, childSpan := tracer.Start(ctx, "DB SELECT cart_items")
defer childSpan.End()
childSpan.SetAttributes(
attribute.String("db.system", "postgresql"),
attribute.String("db.statement", "SELECT * FROM carts WHERE id = $1"),
)
time.Sleep(time.Duration(10+rand.Intn(50)) * time.Millisecond)
}()
// Simulate Payment gateway child span
func() {
_, childSpan := tracer.Start(ctx, "POST https://api.stripe.com/v3/charges")
defer childSpan.End()
time.Sleep(time.Duration(120+rand.Intn(100)) * time.Millisecond)
}()
w.Write([]byte("Order placed! Open trace.html in your browser."))
})
fmt.Println("Server running on http://localhost:8080")
fmt.Println("Send requests to http://localhost:8080/checkout to generate trace.html")
_ = http.ListenAndServe(":8080", nil)
}
Architecture
Standard tracing exporters send serialized protobuf or JSON spans over HTTP/gRPC networks to centralized servers.
otelfile intercepts spans during the batch flush. Rather
than making outbound network connections, it formats the spans into a
flattened, serialized JSON string. It then injects this payload
directly into the bottom of a hardcoded, highly styled browser
template, writing the combined page onto your local disk.
Eviction Strategy
Leaving local development apps running for long periods can result in infinite file sizes. To manage this safely:
- Spans are tracked and grouped internally by TraceID.
- When unique traces exceed your configured limit, the exporter purges the oldest stored trace IDs from memory.
- This prevents your disk file from bloating and ensures quick browser loading times.
FAQ
Is this suitable for production environments?
No. Writing synchronous I/O updates to disk and keeping a rolling trace cache in application RAM is an anti-pattern for large production applications. This tool is designed explicitly to run inside active local dev loops.
How do I share my trace?
Because the generated HTML file is completely self-contained and loads the database locally from embedded JSON blocks, you can simply share the `.html` file over Slack, email, or attach it to a pull request. The receiver can double-click it on their machine to view the exact waterfall chart.