Skip to content

Commit 969c26d

Browse files
committed
Merge branch 'moonshot-xxii-debugger' of github.com:launchdarkly/ldcli into moonshot-xxii-debugger
2 parents 688d8fb + 432c411 commit 969c26d

File tree

12 files changed

+523
-17
lines changed

12 files changed

+523
-17
lines changed

internal/dev_server/dev_server.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dev_server
33
import (
44
"context"
55
"fmt"
6+
"github.com/launchdarkly/ldcli/internal/dev_server/events_db"
67
"log"
78
"net/http"
89
"os"
@@ -52,6 +53,12 @@ func (c LDClient) RunServer(ctx context.Context, serverParams ServerParams) {
5253
if err != nil {
5354
log.Fatal(err)
5455
}
56+
57+
sqlEventStore, err := events_db.NewSqlite(ctx, getEventsDBPath())
58+
if err != nil {
59+
log.Fatal(err)
60+
}
61+
5562
observers := model.NewObservers()
5663
ss := api.NewStrictServer()
5764
apiServer := api.NewStrictHandlerWithOptions(ss, nil, api.StrictHTTPServerOptions{
@@ -60,6 +67,7 @@ func (c LDClient) RunServer(ctx context.Context, serverParams ServerParams) {
6067
})
6168
r := mux.NewRouter()
6269
r.Use(adapters.Middleware(*ldClient, serverParams.DevStreamURI))
70+
r.Use(model.EventStoreMiddleware(sqlEventStore))
6371
r.Use(model.StoreMiddleware(sqlStore))
6472
r.Use(model.ObserversMiddleware(observers))
6573
r.Handle("/", http.RedirectHandler("/ui/", http.StatusFound))
@@ -101,3 +109,11 @@ func getDBPath() string {
101109
}
102110
return dbFilePath
103111
}
112+
func getEventsDBPath() string {
113+
dbFilePath, err := xdg.StateFile("ldcli/dev_server_events.db")
114+
log.Printf("Using database at %s", dbFilePath)
115+
if err != nil {
116+
log.Fatalf("Unable to create state directory: %s", err)
117+
}
118+
return dbFilePath
119+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package store provides database storage for package model
2+
package events_db
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package events_db
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"encoding/json"
7+
"time"
8+
9+
"github.com/launchdarkly/ldcli/internal/dev_server/model"
10+
_ "github.com/mattn/go-sqlite3"
11+
)
12+
13+
type Sqlite struct {
14+
database *sql.DB
15+
dbPath string
16+
}
17+
18+
func (s *Sqlite) CreateDebugSession(ctx context.Context, debugSessionKey string) error {
19+
_, err := s.database.ExecContext(ctx, `
20+
INSERT INTO debug_session (key)
21+
VALUES (?)`, debugSessionKey)
22+
return err
23+
}
24+
25+
func (s *Sqlite) WriteEvent(ctx context.Context, debugSessionKey string, kind string, data json.RawMessage) error {
26+
_, err := s.database.ExecContext(ctx, `
27+
INSERT INTO debug_events (kind, debug_session_key, data)
28+
VALUES (?,?, ?)`, kind, debugSessionKey, data)
29+
return err
30+
}
31+
32+
func (s *Sqlite) QueryEvents(ctx context.Context, debugSessionKey string, kind *string, limit int, offset int) (*model.EventsPage, error) {
33+
// Build the query based on whether kind filter is provided
34+
var query string
35+
var args []interface{}
36+
37+
if kind != nil {
38+
query = `
39+
SELECT id, written_at, kind, data
40+
FROM debug_events
41+
WHERE
42+
debug_session_key = ?
43+
AND kind = ?
44+
ORDER BY id DESC
45+
LIMIT ? OFFSET ?`
46+
args = []interface{}{debugSessionKey, *kind, limit, offset}
47+
} else {
48+
query = `
49+
SELECT id, written_at, kind, data
50+
FROM debug_events
51+
where debug_session_key = ?
52+
ORDER BY id DESC
53+
LIMIT ? OFFSET ?`
54+
args = []interface{}{debugSessionKey, limit, offset}
55+
}
56+
57+
// Execute the main query
58+
rows, err := s.database.QueryContext(ctx, query, args...)
59+
if err != nil {
60+
return nil, err
61+
}
62+
defer rows.Close()
63+
64+
var events []model.Event
65+
for rows.Next() {
66+
var event model.Event
67+
var writtenAtStr string
68+
69+
err := rows.Scan(&event.ID, &writtenAtStr, &event.Kind, &event.Data)
70+
if err != nil {
71+
return nil, err
72+
}
73+
74+
// Parse the timestamp - SQLite returns ISO 8601 format
75+
event.WrittenAt, err = time.Parse(time.RFC3339, writtenAtStr)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
events = append(events, event)
81+
}
82+
83+
if err = rows.Err(); err != nil {
84+
return nil, err
85+
}
86+
87+
// Get total count for pagination info
88+
var totalCount int64
89+
var countQuery string
90+
var countArgs []interface{}
91+
92+
if kind != nil {
93+
countQuery = `SELECT COUNT(*) FROM debug_events WHERE kind = ?`
94+
countArgs = []interface{}{*kind}
95+
} else {
96+
countQuery = `SELECT COUNT(*) FROM debug_events`
97+
countArgs = []interface{}{}
98+
}
99+
100+
err = s.database.QueryRowContext(ctx, countQuery, countArgs...).Scan(&totalCount)
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
// Determine if there are more results
106+
hasMore := int64(offset+len(events)) < totalCount
107+
108+
return &model.EventsPage{
109+
Events: events,
110+
TotalCount: totalCount,
111+
HasMore: hasMore,
112+
}, nil
113+
}
114+
115+
var _ model.EventStore = &Sqlite{}
116+
117+
func NewSqlite(ctx context.Context, dbPath string) (*Sqlite, error) {
118+
store := new(Sqlite)
119+
store.dbPath = dbPath
120+
db, err := sql.Open("sqlite3", dbPath)
121+
if err != nil {
122+
return &Sqlite{}, err
123+
}
124+
store.database = db
125+
err = store.runMigrations(ctx)
126+
if err != nil {
127+
return &Sqlite{}, err
128+
}
129+
return store, nil
130+
}
131+
132+
func (s *Sqlite) runMigrations(ctx context.Context) error {
133+
tx, err := s.database.BeginTx(ctx, nil)
134+
if err != nil {
135+
return err
136+
}
137+
defer func() {
138+
if err != nil {
139+
_ = tx.Rollback()
140+
}
141+
}()
142+
_, err = tx.Exec(`
143+
CREATE TABLE IF NOT EXISTS debug_session (
144+
key text PRIMARY KEY,
145+
written_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
146+
)`)
147+
_, err = tx.Exec(`
148+
CREATE TABLE IF NOT EXISTS debug_events (
149+
id INTEGER PRIMARY KEY AUTOINCREMENT,
150+
written_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
151+
kind text,
152+
data jsonb NOT NULL,
153+
debug_session_key TEXT NOT NULL,
154+
FOREIGN KEY (debug_session_key) REFERENCES debug_session (key) ON DELETE CASCADE
155+
)`)
156+
if err != nil {
157+
return err
158+
}
159+
160+
return tx.Commit()
161+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package events_db_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
_ "embed"
9+
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/launchdarkly/ldcli/internal/dev_server/events_db"
13+
)
14+
15+
//go:embed testevent.json
16+
var testEvent string
17+
18+
func TestDBFunctions(t *testing.T) {
19+
ctx := context.Background()
20+
dbName := "events_test.db"
21+
defer func() {
22+
require.NoError(t, os.Remove(dbName))
23+
}()
24+
25+
store, err := events_db.NewSqlite(ctx, dbName)
26+
require.NoError(t, err)
27+
28+
require.NotNil(t, store)
29+
30+
debugSessionKey := "test"
31+
32+
t.Run("CreateDebugSession succeeds", func(t *testing.T) {
33+
err := store.CreateDebugSession(ctx, debugSessionKey)
34+
require.NoError(t, err)
35+
36+
// Test that creating the same session again error
37+
err = store.CreateDebugSession(ctx, debugSessionKey)
38+
require.Error(t, err)
39+
40+
// Test creating a different session
41+
err = store.CreateDebugSession(ctx, "another-session")
42+
require.NoError(t, err)
43+
})
44+
45+
t.Run("WriteEvent succeeds", func(t *testing.T) {
46+
err := store.WriteEvent(ctx, debugSessionKey, "summary", []byte(testEvent))
47+
require.NoError(t, err)
48+
})
49+
50+
t.Run("QueryEvents with no filter", func(t *testing.T) {
51+
// Write some test events
52+
err := store.WriteEvent(ctx, debugSessionKey, "summary", []byte(testEvent))
53+
require.NoError(t, err)
54+
err = store.WriteEvent(ctx, debugSessionKey, "diagnostic", []byte(`{"kind":"diagnostic","data":"test"}`))
55+
require.NoError(t, err)
56+
err = store.WriteEvent(ctx, debugSessionKey, "summary", []byte(testEvent))
57+
require.NoError(t, err)
58+
59+
// Query all events
60+
page, err := store.QueryEvents(ctx, debugSessionKey, nil, 10, 0)
61+
require.NoError(t, err)
62+
require.NotNil(t, page)
63+
require.Len(t, page.Events, 4) // 3 new + 1 from previous test
64+
require.Equal(t, int64(4), page.TotalCount)
65+
require.False(t, page.HasMore)
66+
})
67+
68+
t.Run("QueryEvents with kind filter", func(t *testing.T) {
69+
kind := "summary"
70+
page, err := store.QueryEvents(ctx, debugSessionKey, &kind, 10, 0)
71+
require.NoError(t, err)
72+
require.NotNil(t, page)
73+
require.Len(t, page.Events, 3) // Only summary events
74+
require.Equal(t, int64(3), page.TotalCount)
75+
require.False(t, page.HasMore)
76+
77+
// Verify all returned events have the correct kind
78+
for _, event := range page.Events {
79+
require.Equal(t, "summary", event.Kind)
80+
}
81+
})
82+
83+
t.Run("QueryEvents with pagination", func(t *testing.T) {
84+
// Query with limit
85+
page, err := store.QueryEvents(ctx, debugSessionKey, nil, 2, 0)
86+
require.NoError(t, err)
87+
require.NotNil(t, page)
88+
require.Len(t, page.Events, 2)
89+
require.Equal(t, int64(4), page.TotalCount)
90+
require.True(t, page.HasMore)
91+
92+
// Query next page
93+
page, err = store.QueryEvents(ctx, debugSessionKey, nil, 2, 2)
94+
require.NoError(t, err)
95+
require.NotNil(t, page)
96+
require.Len(t, page.Events, 2)
97+
require.Equal(t, int64(4), page.TotalCount)
98+
require.False(t, page.HasMore)
99+
})
100+
101+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"kind": "summary",
3+
"startDate": 1754494042849,
4+
"endDate": 1754494042962,
5+
"features": {
6+
"enable-datadog-profiling": {
7+
"default": false,
8+
"counters": [
9+
{
10+
"variation": 0,
11+
"version": 50,
12+
"value": true,
13+
"count": 1
14+
}
15+
],
16+
"contextKinds": [
17+
"application"
18+
]
19+
},
20+
"cpu-profile-dump-wait-minutes": {
21+
"default": 60,
22+
"counters": [
23+
{
24+
"variation": 0,
25+
"version": 6,
26+
"value": 30,
27+
"count": 1
28+
}
29+
],
30+
"contextKinds": [
31+
"user"
32+
]
33+
},
34+
"cpu-profile-threshold": {
35+
"default": 1,
36+
"counters": [
37+
{
38+
"variation": 0,
39+
"version": 15,
40+
"value": 0.8,
41+
"count": 1
42+
}
43+
],
44+
"contextKinds": [
45+
"user"
46+
]
47+
},
48+
"cpu-profile-dump-duration-seconds": {
49+
"default": 30,
50+
"counters": [
51+
{
52+
"variation": 0,
53+
"version": 7,
54+
"value": 30,
55+
"count": 1
56+
}
57+
],
58+
"contextKinds": [
59+
"user"
60+
]
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)