Files
go-otel/internal/todos/todos.go
T
mats c8cc0fb28a
Build and Publish / Metadata (push) Successful in 8s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 2m7s
update
2026-09-21 19:38:09 +02:00

156 lines
4.1 KiB
Go

// Package todos stores todo/issue notes filed from the browser by the
// otel-todo.js widget.
package todos
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"sort"
"sync"
"time"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
)
// Todo is one note filed from a page. Project is the RUM service name.
type Todo struct {
ID string `json:"id"`
Project string `json:"project"`
Text string `json:"text"`
Component string `json:"component,omitempty"`
Path string `json:"path,omitempty"`
Hash string `json:"hash,omitempty"`
URL string `json:"url,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
Session string `json:"session,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
// Store persists todos. List returns newest first; an empty project means all.
type Store interface {
Add(ctx context.Context, t Todo) error
List(ctx context.Context, project string, limit int) ([]Todo, error)
}
// FileStore keeps todos in memory and appends them to a JSONL file.
type FileStore struct {
mu sync.Mutex
path string
items []Todo
}
func NewFileStore(path string) (*FileStore, error) {
fs := &FileStore{path: path}
f, err := os.Open(path)
if os.IsNotExist(err) {
return fs, nil
}
if err != nil {
return nil, err
}
defer f.Close()
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 64<<10), 1<<20)
for sc.Scan() {
var t Todo
if json.Unmarshal(sc.Bytes(), &t) == nil {
fs.items = append(fs.items, t)
}
}
return fs, sc.Err()
}
func (s *FileStore) Add(_ context.Context, t Todo) error {
line, err := json.Marshal(t)
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
f, err := os.OpenFile(s.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
if _, err := f.Write(append(line, '\n')); err != nil {
return err
}
s.items = append(s.items, t)
return nil
}
func (s *FileStore) List(_ context.Context, project string, limit int) ([]Todo, error) {
s.mu.Lock()
defer s.mu.Unlock()
var out []Todo
for _, t := range s.items {
if project == "" || t.Project == project {
out = append(out, t)
}
}
sort.SliceStable(out, func(i, j int) bool { return out[i].CreatedAt.After(out[j].CreatedAt) })
if limit > 0 && len(out) > limit {
out = out[:limit]
}
return out, nil
}
// ClickHouseStore persists todos in a plain MergeTree table (no TTL).
type ClickHouseStore struct {
conn driver.Conn
db string
}
func NewClickHouseStore(ctx context.Context, conn driver.Conn, db string) (*ClickHouseStore, error) {
err := conn.Exec(ctx, fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s.todos (
id String,
project LowCardinality(String),
text String,
component String,
path String,
hash String,
url String,
user_agent String,
session String,
created_at DateTime64(3, 'UTC')
) ENGINE = MergeTree
ORDER BY (project, created_at)`, db))
if err != nil {
return nil, err
}
return &ClickHouseStore{conn: conn, db: db}, nil
}
func (s *ClickHouseStore) Add(ctx context.Context, t Todo) error {
return s.conn.Exec(ctx, fmt.Sprintf(`INSERT INTO %s.todos (id, project, text, component, path, hash, url, user_agent, session, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, s.db),
t.ID, t.Project, t.Text, t.Component, t.Path, t.Hash, t.URL, t.UserAgent, t.Session, t.CreatedAt.UTC())
}
func (s *ClickHouseStore) List(ctx context.Context, project string, limit int) ([]Todo, error) {
q := fmt.Sprintf(`SELECT id, project, text, component, path, hash, url, user_agent, session, created_at FROM %s.todos`, s.db)
var args []any
if project != "" {
q += ` WHERE project = ?`
args = append(args, project)
}
q += ` ORDER BY created_at DESC LIMIT ?`
args = append(args, limit)
rows, err := s.conn.Query(ctx, q, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Todo
for rows.Next() {
var t Todo
if err := rows.Scan(&t.ID, &t.Project, &t.Text, &t.Component, &t.Path, &t.Hash, &t.URL, &t.UserAgent, &t.Session, &t.CreatedAt); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}