Learn
Embedded Mode

Embedded Mode

Embedded mode runs a full Traceway server inside your Go process, using SQLite for all storage (relational data and telemetry). No Docker, no infrastructure: just one tracewaybackend.Run() call and your app has local observability.

This is intended for local development only. For production, use the Docker Compose or minimal deployment.

Telemetry storage follows your build tags: a plain go build embeds SQLite, and building your app with -tags telemetry_duckdb (CGO required) swaps the embedded telemetry store to DuckDB's columnar engine. See Build Tags.

Want to see it in action? Check out the full working example on GitHub (opens in a new tab): a complete Go app with embedded Traceway and OpenTelemetry instrumentation you can run in one command.

Quick Start

Install the Traceway backend module

go get github.com/tracewayapp/traceway/backend
⚠️

On backend v1.9.15 and earlier, that command fails. Those releases declare their dependency on the Traceway CLI module with a placeholder version, so go get stops with:

github.com/tracewayapp/traceway/cli@v0.0.0-00010101000000-000000000000: invalid version: unknown revision 000000000000

Point that dependency at a real published CLI version in your own go.mod:

replace github.com/tracewayapp/traceway/cli => github.com/tracewayapp/traceway/cli v1.9.15

Then run go get github.com/tracewayapp/traceway/backend again. The failed attempt leaves your go.mod untouched, so there is nothing to clean up first. The CLI and the backend are released together, so use the CLI version that matches the backend version you want. A replace only applies to your own module, so it does not affect anything that imports your code.

Later releases need none of this. Upgrade instead if you can.

Start Traceway inside your app

Call tracewaybackend.Run() in a goroutine during startup. It boots an in-memory SQLite server and seeds a default user and project:

package main
 
import (
	"github.com/gin-gonic/gin"
	tracewaybackend "github.com/tracewayapp/traceway/backend"
)
 
func main() {
	go tracewaybackend.Run(
		tracewaybackend.WithPort(8082),
		tracewaybackend.WithDefaultUser("admin@localhost.com", "admin"),
		tracewaybackend.WithDefaultProject("My App", "opentelemetry", "dev-token"),
	)
 
	router := gin.Default()
	// ... your routes
	router.Run(":8080")
}

Point OpenTelemetry to your local Traceway

Install the OTel dependencies:

go get go.opentelemetry.io/otel \
       go.opentelemetry.io/otel/sdk \
       go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \
       go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin

Configure the OTLP exporter to send traces to the embedded server, and add the OTel middleware to your router:

package main
 
import (
	"context"
	"time"
 
	"github.com/gin-gonic/gin"
	tracewaybackend "github.com/tracewayapp/traceway/backend"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"go.opentelemetry.io/otel"
	"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.26.0"
)
 
const tracewayToken = "dev-token"
 
func main() {
	go tracewaybackend.Run(
		tracewaybackend.WithPort(8082),
		tracewaybackend.WithDefaultUser("admin@localhost.com", "admin"),
		tracewaybackend.WithDefaultProject("My App", "opentelemetry", tracewayToken),
	)
 
	shutdown := initTracer()
	defer shutdown()
 
	router := gin.Default()
	router.Use(otelgin.Middleware("my-app"))
 
	router.GET("/ping", func(c *gin.Context) {
		c.String(200, "pong")
	})
	router.Run(":8080")
}
 
func initTracer() func() {
	exporter, err := otlptracehttp.New(
		context.Background(),
		otlptracehttp.WithEndpointURL("http://localhost:8082/api/otel/v1/traces"),
		otlptracehttp.WithHeaders(map[string]string{
			"Authorization": "Bearer " + tracewayToken,
		}),
	)
	if err != nil {
		panic(err)
	}
 
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceName("my-app"),
		)),
	)
	otel.SetTracerProvider(tp)
 
	// Go's global propagator is a no-op by default. Without this line the
	// traceparent header is never sent or read, so an outgoing call starts a
	// brand new trace instead of joining the request it came from.
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
		propagation.TraceContext{},
		propagation.Baggage{},
	))
 
	return func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		tp.Shutdown(ctx)
	}
}

The key parts:

  • Exporter URL points to http://localhost:8082/api/otel/v1/traces, the embedded server's OTLP endpoint
  • Authorization header uses the same token from WithDefaultProject
  • otel.SetTextMapPropagator is required in Go. The global propagator ships as a no-op, so without this call an HTTP call your app makes to another service shows up as its own separate trace instead of a child span
  • defer shutdown() flushes the batch processor on exit. Without it the last few seconds of spans never leave the process

Open the dashboard

Run your app, then open http://localhost:8082 (opens in a new tab) in your browser.

Log in with the credentials from WithDefaultUser (admin@localhost.com / admin).

Hit your app at http://localhost:8080/ping a few times. Traces will appear in the Traceway dashboard within a few seconds.

Configuration Options

All options are passed to tracewaybackend.Run(). When any options are provided, embedded mode is activated automatically (pure SQLite).

OptionDescriptionDefault
WithPort(port)HTTP port for the Traceway server8082
WithServerURL(url)Base URL override (used in connection string output)http://localhost:{port}
WithSQLitePath(path)SQLite database file path:memory: (in-memory)
WithDefaultUser(email, password)Create a default admin user on startupNone
WithDefaultProject(name, framework, token)Create a default project with a fixed token (can be called multiple times)None
DisableLogging()Silence the embedded server's startup and request logs so they don't mix with your app's outputLogging enabled
WithDefaultProjectSourceMapToken(name, token)Set a fixed source map upload token on a default project. Call it after the matching WithDefaultProject, since it looks the project up by nameNone
WithMonitoringURL(connectionString)Report the embedded server's own telemetry to another Traceway instance. Takes a connection string in the form <project_token>@<url>/api/reportNone

If the embedded server's startup lines get in the way of your own output, add tracewaybackend.DisableLogging() to the option list:

go tracewaybackend.Run(
	tracewaybackend.WithPort(8082),
	tracewaybackend.DisableLogging(),
	tracewaybackend.WithDefaultUser("admin@localhost.com", "admin"),
	tracewaybackend.WithDefaultProject("My App", "opentelemetry", "dev-token"),
)

Multiple Projects

WithDefaultProject can be called multiple times to create separate projects: one for your Go backend, another for a frontend or any other service. Each project gets its own token, keeping telemetry organized in the dashboard:

go tracewaybackend.Run(
	tracewaybackend.WithPort(8082),
	tracewaybackend.WithDefaultUser("admin@localhost.com", "admin"),
	tracewaybackend.WithDefaultProject("Backend", "opentelemetry", "backend-token"),
	tracewaybackend.WithDefaultProject("Frontend", "svelte", "frontend-token"),
)

Data Persistence

By default, the database is in-memory, which means all data is lost when your application restarts. This is fine for quick debugging sessions, but if you want data to survive restarts, set a file path:

Add WithSQLitePath to the options you already pass. It does not replace them. Keep the go keyword too, otherwise Run blocks and your own server never starts.

go tracewaybackend.Run(
	tracewaybackend.WithPort(8082),
	tracewaybackend.WithSQLitePath("./traceway.db"),
	tracewaybackend.WithDefaultUser("admin@localhost.com", "admin"),
	tracewaybackend.WithDefaultProject("My App", "opentelemetry", "dev-token"),
)

What ends up on disk

WithSQLitePath("./traceway.db") creates two databases side by side, not one:

FileContents
traceway.dbRelational data: users, organizations, projects, settings
traceway_telemetry.dbTelemetry: traces, endpoints, issues, logs, metrics

SQLite also writes -wal and -shm sidecar files for each database. Back up or ignore all of them, not just the file you named. The server also creates a storage/ directory in the process working directory for session recordings and synthetics screenshots. That directory appears in the default in-memory mode too.

Add all of it to .gitignore:

traceway.db*
traceway_telemetry.db*
storage/

Seeding is idempotent: if the user from WithDefaultUser already exists, it's skipped.

Development-Only Guard

A common pattern is guarding embedded mode behind an environment check so it only runs in development:

if os.Getenv("ENV") != "production" {
	go tracewaybackend.Run(
		tracewaybackend.WithPort(8082),
		tracewaybackend.WithDefaultUser("admin@localhost.com", "admin"),
		tracewaybackend.WithDefaultProject("My App", "opentelemetry", "dev-token"),
	)
}

Full Example

For a complete working example with error recording and child spans, see the embedded-backend-otel example on GitHub (opens in a new tab).

Next Steps

The Quick Start above only sends request traces. Everything else works the same against the embedded server as against a hosted one, so point these guides at http://localhost:8082 and use your WithDefaultProject token:

  • Traces: child spans, recording errors so they land in Issues with a stack trace, and background jobs. A background job span needs SpanKind.CONSUMER, a plain root span is silently dropped.
  • Logs: emitting logs that attach to the request's trace.
  • Metrics: counters, histograms, and how Traceway stores them.