diff --git a/.gitignore b/.gitignore index e88f078..20c8aec 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,7 @@ otel-desktop-viewer desktopexporter/internal/app/node_modules .vscode/ -duck.* +*.db +*.wal help -# dist/ -# distribution/linux/* -# distribution/darwin/* go.work* diff --git a/Makefile b/Makefile index acdc5f5..933aa52 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,10 @@ test-go: run-go: SERVE_FROM_FS=true cd desktopcollector; go run ./... +.PHONY: run-db-go +run-db-go: + SERVE_FROM_FS=true cd desktopcollector; go run ./... --db ../duck.db + .PHONY: build-js build-js: cd desktopexporter/internal/app; npx esbuild --bundle main.tsx main.css --outdir=../server/static diff --git a/desktopcollector/main.go b/desktopcollector/main.go index adc1194..7758a1c 100644 --- a/desktopcollector/main.go +++ b/desktopcollector/main.go @@ -53,7 +53,7 @@ func runInteractive(params otelcol.CollectorSettings) error { func newCommand(set otelcol.CollectorSettings) *cobra.Command { var httpPortFlag, grpcPortFlag, browserPortFlag int - var hostFlag string + var hostFlag, dbFlag string rootCmd := &cobra.Command{ Use: set.BuildInfo.Command, @@ -65,6 +65,7 @@ func newCommand(set otelcol.CollectorSettings) *cobra.Command { `yaml:receivers::otlp::protocols::grpc::endpoint: ` + hostFlag + `:` + strconv.Itoa(grpcPortFlag), `yaml:exporters::desktop:`, `yaml:exporters::desktop::endpoint: ` + hostFlag + `:` + strconv.Itoa(browserPortFlag), + `yaml:exporters::desktop::db: ` + dbFlag, `yaml:service::pipelines::traces::receivers: [otlp]`, `yaml:service::pipelines::traces::exporters: [desktop]`, `yaml:service::pipelines::metrics::receivers: [otlp]`, @@ -85,6 +86,7 @@ func newCommand(set otelcol.CollectorSettings) *cobra.Command { rootCmd.Flags().IntVar(&grpcPortFlag, "grpc", 4317, "The port number on which we listen for OTLP grpc payloads") rootCmd.Flags().IntVar(&browserPortFlag, "browser", 8000, "The port number where we expose our data") rootCmd.Flags().StringVar(&hostFlag, "host", "localhost", "The host where we expose our all endpoints (OTLP receivers and browser)") + rootCmd.Flags().StringVar(&dbFlag, "db", "", "The path of your database file. Omitting this flag opens DuckDB in in-memory mode, with no data persisted to disk.") return rootCmd } diff --git a/desktopexporter/config.go b/desktopexporter/config.go index 736b5d1..84ec334 100644 --- a/desktopexporter/config.go +++ b/desktopexporter/config.go @@ -8,6 +8,9 @@ import ( type Config struct { // Endpoint defines the host and port where we serve our frontend app Endpoint string `mapstructure:"endpoint"` + + // Endpoint defines the path of your database file. Setting an enpty string opens DuckDB in in-memory mode + DbPath string `mapstructure:"db"` } // Validate checks if the exporter configuration is valid diff --git a/desktopexporter/exporter.go b/desktopexporter/exporter.go index 51b8389..a057859 100644 --- a/desktopexporter/exporter.go +++ b/desktopexporter/exporter.go @@ -20,7 +20,7 @@ type desktopExporter struct { } func newDesktopExporter(cfg *Config) *desktopExporter { - server := server.NewServer(cfg.Endpoint) + server := server.NewServer(cfg.Endpoint, cfg.DbPath) return &desktopExporter{ server: server, } diff --git a/desktopexporter/internal/server/server.go b/desktopexporter/internal/server/server.go index 1ec25df..55f5841 100644 --- a/desktopexporter/internal/server/server.go +++ b/desktopexporter/internal/server/server.go @@ -25,12 +25,12 @@ type Server struct { Store *store.Store } -func NewServer(endpoint string) *Server { +func NewServer(endpoint string, dbPath string) *Server { s := Server{ server: http.Server{ Addr: endpoint, }, - Store: store.NewStore(context.Background()), + Store: store.NewStore(context.Background(), dbPath), } serveFromFS, err := strconv.ParseBool(os.Getenv("SERVE_FROM_FS")) diff --git a/desktopexporter/internal/server/server_test.go b/desktopexporter/internal/server/server_test.go index efece4d..636f72f 100644 --- a/desktopexporter/internal/server/server_test.go +++ b/desktopexporter/internal/server/server_test.go @@ -16,7 +16,7 @@ import ( ) func setupEmpty() (*httptest.Server, func()) { - server := NewServer("localhost:8000") + server := NewServer("localhost:8000", "") testServer := httptest.NewServer(server.Handler(false)) return testServer, func() { @@ -26,7 +26,7 @@ func setupEmpty() (*httptest.Server, func()) { } func setupWithTrace(t *testing.T) (*httptest.Server, func(*testing.T)) { - server := NewServer("localhost:8000") + server := NewServer("localhost:8000", "") testSpanData := telemetry.SpanData{ TraceID: "1234567890", TraceState: "", @@ -63,6 +63,7 @@ func setupWithTrace(t *testing.T) (*httptest.Server, func(*testing.T)) { server.Store.Close() } } + func TestTracesHandler(t *testing.T) { t.Run("Traces Handler (Empty)", func(t *testing.T) { testServer, teardown := setupEmpty() diff --git a/desktopexporter/internal/store/store.go b/desktopexporter/internal/store/store.go index 3b294f9..d036763 100644 --- a/desktopexporter/internal/store/store.go +++ b/desktopexporter/internal/store/store.go @@ -21,8 +21,9 @@ type Store struct { conn driver.Conn } -func NewStore(ctx context.Context) *Store { - connector, err := duckdb.NewConnector("", nil) +func NewStore(ctx context.Context, dbPath string) *Store { + connector, err := duckdb.NewConnector(dbPath, nil) + if err != nil { log.Fatalf("could not initialize new connector: %s", err.Error()) } diff --git a/desktopexporter/internal/store/store_test.go b/desktopexporter/internal/store/store_test.go new file mode 100644 index 0000000..1948b1b --- /dev/null +++ b/desktopexporter/internal/store/store_test.go @@ -0,0 +1,49 @@ +package store + +import ( + "context" + "os" + "testing" + + "github.com/CtrlSpice/otel-desktop-viewer/desktopexporter/internal/telemetry" + "github.com/stretchr/testify/assert" +) + +func TestPersistence(t *testing.T) { + ctx := context.Background() + store := NewStore(ctx, "./quack.db") + + // Check that db file is created properly + _, err := os.Stat("./quack.db") + assert.NoErrorf(t, err, "database file does not exist: %v", err) + + // Add sample spans to the store + err = store.AddSpans(ctx, telemetry.NewSampleTelemetry().Spans) + assert.NoErrorf(t, err, "could not add spans to the database: %v", err) + + // Get trace summaries and check length + summaries, err := store.GetTraceSummaries(ctx) + if assert.NoErrorf(t, err, "could not get trace summaries: %v", err) { + assert.Len(t, *summaries, 2) + } + + // Close store + err = store.Close() + assert.NoErrorf(t, err, "could not close database: %v", err) + + // Reopen store from the database file + store = NewStore(ctx, "./quack.db") + + // Get a trace by ID and check ID of root span + trace, err := store.GetTrace(ctx, "42957c7c2fca940a0d32a0cdd38c06a4") + if assert.NoErrorf(t, err, "could not get trace: %v", err) { + assert.Equal(t, "37fd1349bf83d330", trace.Spans[0].SpanID) + } + + // Clean up + err = store.Close() + assert.NoErrorf(t, err, "could not close database: %v", err) + + err = os.Remove("./quack.db") + assert.NoError(t, err, "could not remove database file: %v", err) +}