Skip to content

Commit 739898f

Browse files
authored
chore: add queries to fetch tool logs from clickhouse (#1029)
We want to expose logs from Clickhouse to our dashboard (and any users that might want them). This adds a query to fetch the logs with basic filtering and pagination. SQLC doesn't support clickhouse so I tried to keep something consistent with our SQLC definitions: - We still write CH queries in a `queries.sql` file - We can then ask Claude to autogenerate the code for us It's not ideal, but it's a way to still keep something consistent while taking advantage of some code generation. I've added a readme and instructions so Claude can work nicely with it. Note: the folder structure isn't very consistent with the rest of our codebase, but I'm going to refactor this (adding `repo` folder and moving the `impl.go` logic from `log/` to here).
1 parent e8421a4 commit 739898f

File tree

5 files changed

+694
-0
lines changed

5 files changed

+694
-0
lines changed

CLAUDE.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,79 @@ You are an expert AI programming assistant specializing in building APIs with Go
151151
- Always call out when making a backwards incompatible schema change.
152152
- Suggest running `mise db:diff <migration-name>` after making schema changes to generate a migration file. Replace `<migration-name>` with a clear snake-case migration id such as `users-add-email-column`.
153153

154+
## ClickHouse Queries (Telemetry Package)
155+
156+
The `server/internal/telemetry` package uses ClickHouse for high-performance analytics queries. Unlike PostgreSQL queries, **ClickHouse queries are NOT auto-generated by sqlc** (sqlc doesn't support ClickHouse), but we follow sqlc conventions for consistency.
157+
158+
### File Structure
159+
160+
- **`queries.sql`**: Human-readable SQL queries with sqlc-style `-- name:` comments
161+
- **`queries.sql.go`**: Manual Go implementations following sqlc patterns
162+
- **`README.md`**: Detailed documentation and patterns specific to ClickHouse
163+
164+
### Adding ClickHouse Queries
165+
166+
When asked to add a new ClickHouse query to the telemetry package:
167+
168+
1. **Add the query to `queries.sql`** with sqlc formatting:
169+
```sql
170+
-- name: GetMetrics :many
171+
select * from metrics where project_id = ? limit ?;
172+
```
173+
174+
2. **Implement in `queries.sql.go`** following the pattern:
175+
```go
176+
const getMetrics = `-- name: GetMetrics :many
177+
select * from metrics where project_id = ? limit ?
178+
`
179+
180+
type GetMetricsParams struct {
181+
ProjectID string
182+
Limit int
183+
}
184+
185+
func (q *Queries) GetMetrics(ctx context.Context, arg GetMetricsParams) ([]Metric, error) {
186+
// Implementation following existing patterns
187+
}
188+
```
189+
190+
3. **Follow ClickHouse-specific patterns**:
191+
- Use `?` placeholders (not `$1, $2, ...`)
192+
- For optional filters: `(? = '' or field = ?)` pattern, pass value twice
193+
- For cursor pagination: Use nil UUID sentinel (`00000000-0000-0000-0000-000000000000`)
194+
- For UUIDv7 cursors: Use `UUIDv7ToDateTime(toUUID(?))` to extract timestamp
195+
- No short-circuit evaluation: Use `IF()` function instead of `OR` when needed
196+
197+
### Example: Optional Filters
198+
199+
```sql
200+
where project_id = ?
201+
and (? = '' or deployment_id = ?) -- Pass empty string or value twice
202+
```
203+
204+
```go
205+
arg.DeploymentID, arg.DeploymentID // Pass twice for the pattern
206+
```
207+
208+
### Example: Cursor Pagination
209+
210+
```sql
211+
and if(
212+
toUUID(?) = toUUID('00000000-0000-0000-0000-000000000000'),
213+
true,
214+
if(? = 'ASC', timestamp > UUIDv7ToDateTime(toUUID(?)), timestamp < UUIDv7ToDateTime(toUUID(?)))
215+
)
216+
```
217+
218+
### Testing ClickHouse Queries
219+
220+
- Use `testenv.Launch()` in `TestMain` for infrastructure setup
221+
- Add `time.Sleep(100 * time.Millisecond)` after inserts (ClickHouse eventual consistency)
222+
- Use table-driven tests with descriptive "it" prefix names
223+
- Create helper functions for test data insertion
224+
225+
See `server/internal/telemetry/README.md` for comprehensive documentation.
226+
154227
# Schema design rules
155228

156229
## Change tracking
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Telemetry Package
2+
3+
This package handles telemetry data storage and retrieval using ClickHouse for high-performance analytics queries.
4+
5+
## ClickHouse Queries
6+
7+
Unlike PostgreSQL queries in other packages, ClickHouse queries are **not auto-generated** by sqlc since sqlc doesn't support ClickHouse. However, we follow sqlc conventions for consistency.
8+
9+
### Query Files
10+
11+
- **`queries.sql`**: Human-readable SQL queries following sqlc conventions with `-- name:` comments
12+
- **`queries.sql.go`**: Manual Go implementations of the queries
13+
14+
### Adding a New Query
15+
16+
1. **Add the query to `queries.sql`** with sqlc-style formatting:
17+
```sql
18+
-- name: GetSomething :one
19+
select * from table where id = ?;
20+
```
21+
22+
2. **Implement the function in `queries.sql.go`**:
23+
- Extract the query as a const with the sqlc comment
24+
- Create a params struct (if needed) right before the function
25+
- Implement the function following existing patterns
26+
27+
3. **Ask Claude Code to generate the implementation** - it will follow the sqlc conventions established in this package.
28+
29+
### Example Pattern
30+
31+
**In `queries.sql`:**
32+
```sql
33+
-- name: ListItems :many
34+
select id, name from items where project_id = ? limit ?;
35+
```
36+
37+
**In `queries.sql.go`:**
38+
```go
39+
const listItems = `-- name: ListItems :many
40+
select id, name from items where project_id = ? limit ?
41+
`
42+
43+
type ListItemsParams struct {
44+
ProjectID string
45+
Limit int
46+
}
47+
48+
func (q *Queries) ListItems(ctx context.Context, arg ListItemsParams) ([]Item, error) {
49+
rows, err := q.conn.Query(ctx, listItems, arg.ProjectID, arg.Limit)
50+
// ... implementation
51+
}
52+
```
53+
54+
## Pagination
55+
56+
This package uses cursor-based pagination with the "limit + 1" pattern:
57+
58+
1. Client requests N items per page
59+
2. Query fetches N+1 items
60+
3. If N+1 items returned → `hasNextPage = true`, return only N items
61+
4. If ≤N items returned → `hasNextPage = false`, return all items
62+
5. Cursor is the ID of the last returned item
63+
64+
### ClickHouse-Specific Patterns
65+
66+
#### Nil UUID Sentinel
67+
ClickHouse doesn't support short-circuit evaluation in OR expressions, so we use the nil UUID (`00000000-0000-0000-0000-000000000000`) as a sentinel value to indicate "no cursor" (first page):
68+
69+
```sql
70+
and if(
71+
toUUID(?) = toUUID('00000000-0000-0000-0000-000000000000'),
72+
true,
73+
-- cursor comparison logic
74+
)
75+
```
76+
77+
#### Optional Filters
78+
Use the pattern `(? = '' or field = ?)` to make filters optional:
79+
80+
```sql
81+
and (? = '' or deployment_id = ?)
82+
```
83+
84+
Pass empty string when filter is not needed, or pass the value twice when filtering:
85+
```go
86+
arg.DeploymentID, arg.DeploymentID // pass twice for the pattern above
87+
```
88+
89+
#### UUIDv7 Timestamp Extraction
90+
Use `UUIDv7ToDateTime(toUUID(?))` to extract the embedded timestamp from UUIDv7 for cursor-based pagination:
91+
92+
```sql
93+
timestamp > UUIDv7ToDateTime(toUUID(?))
94+
```
95+
96+
## Testing
97+
98+
Tests use testcontainers to spin up a real ClickHouse instance. See `list_tool_logs_test.go` for examples.
99+
100+
Key testing patterns:
101+
- Use `testenv.Launch()` in `TestMain` to set up infrastructure
102+
- Create helper functions for inserting test data
103+
- Use table-driven tests with descriptive names
104+
- Add `time.Sleep(100 * time.Millisecond)` after inserts to allow ClickHouse to make data available
105+
106+
## Data Models
107+
108+
See `models.go` for struct definitions with ClickHouse field tags (`ch:"field_name"`).

0 commit comments

Comments
 (0)