150 lines
4.3 KiB
Go
150 lines
4.3 KiB
Go
package collector
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.tornberg.me/go-otel/internal/todos"
|
|
)
|
|
|
|
const (
|
|
maxTodoBody = 16 << 10
|
|
maxTodoText = 4000
|
|
maxTodoMeta = 500
|
|
)
|
|
|
|
// SetTodoStore enables the todo endpoints.
|
|
func (c *Collector) SetTodoStore(s todos.Store) { c.todoStore = s }
|
|
|
|
func clipTo(s string, n int) string {
|
|
s = strings.TrimSpace(s)
|
|
if len(s) > n {
|
|
return s[:n]
|
|
}
|
|
return s
|
|
}
|
|
|
|
type todoPayload struct {
|
|
Project string `json:"project"`
|
|
Text string `json:"text"`
|
|
Component string `json:"component"`
|
|
Path string `json:"path"`
|
|
Hash string `json:"hash"`
|
|
URL string `json:"url"`
|
|
UserAgent string `json:"ua"`
|
|
Session string `json:"session"`
|
|
ElementSelector string `json:"elementSelector"`
|
|
ElementLabel string `json:"elementLabel"`
|
|
}
|
|
|
|
// HandleTodoCreate ingests a todo posted by otel-todo.js. It is guarded by
|
|
// the same ingest key and rate limit as the other RUM endpoints.
|
|
func (c *Collector) HandleTodoCreate(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if c.todoStore == nil {
|
|
http.Error(w, "todos not enabled", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
if !c.checkIngest(w, r) {
|
|
return
|
|
}
|
|
var p todoPayload
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxTodoBody)).Decode(&p); err != nil {
|
|
http.Error(w, "invalid todo payload", http.StatusBadRequest)
|
|
return
|
|
}
|
|
text := clipTo(p.Text, maxTodoText)
|
|
if text == "" {
|
|
http.Error(w, "text is required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
project := clipTo(p.Project, maxTodoMeta)
|
|
if project == "" {
|
|
project = "frontend"
|
|
}
|
|
id := make([]byte, 8)
|
|
_, _ = rand.Read(id)
|
|
t := todos.Todo{
|
|
ID: hex.EncodeToString(id),
|
|
Project: project,
|
|
Text: text,
|
|
Component: clipTo(p.Component, maxTodoMeta),
|
|
Path: clipTo(p.Path, maxTodoMeta),
|
|
Hash: clipTo(p.Hash, maxTodoMeta),
|
|
URL: clipTo(p.URL, maxTodoMeta),
|
|
UserAgent: clipTo(p.UserAgent, maxTodoMeta),
|
|
Session: clipTo(p.Session, maxTodoMeta),
|
|
ElementSelector: clipTo(p.ElementSelector, maxTodoMeta),
|
|
ElementLabel: clipTo(p.ElementLabel, maxTodoMeta),
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
if err := c.todoStore.Add(r.Context(), t); err != nil {
|
|
log.Printf("failed to store todo: %v", err)
|
|
http.Error(w, "failed to store todo", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(t)
|
|
}
|
|
|
|
// HandleTodoList returns todos, newest first. ?project= filters by project
|
|
// (omit for all); ?limit= defaults to 100, max 1000. Registered behind auth.
|
|
func (c *Collector) HandleTodoList(w http.ResponseWriter, r *http.Request) {
|
|
if c.todoStore == nil {
|
|
http.Error(w, "todos not enabled", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
limit := 100
|
|
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 {
|
|
limit = min(v, 1000)
|
|
}
|
|
items, err := c.todoStore.List(r.Context(), r.URL.Query().Get("project"), limit)
|
|
if err != nil {
|
|
log.Printf("failed to list todos: %v", err)
|
|
http.Error(w, "failed to list todos", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if items == nil {
|
|
items = []todos.Todo{}
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(items)
|
|
}
|
|
|
|
// HandleTodoUpdate marks a todo done or open: PATCH /api/todos/{id} {"done": bool}.
|
|
func (c *Collector) HandleTodoUpdate(w http.ResponseWriter, r *http.Request) {
|
|
if c.todoStore == nil {
|
|
http.Error(w, "todos not enabled", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
var req struct {
|
|
Done *bool `json:"done"`
|
|
}
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1024)).Decode(&req); err != nil || req.Done == nil {
|
|
http.Error(w, "body must be {\"done\": true|false}", http.StatusBadRequest)
|
|
return
|
|
}
|
|
t, ok, err := c.todoStore.SetDone(r.Context(), r.PathValue("id"), *req.Done)
|
|
if err != nil {
|
|
log.Printf("failed to update todo: %v", err)
|
|
http.Error(w, "failed to update todo", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if !ok {
|
|
http.Error(w, "todo not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(t)
|
|
}
|