Files
Tomas DvorakandDevin 8cfddf9184 feat: phase 3 observability — IMS telemetry merge
Port IMS telemetry into Primora conventions:

- core.components/core.events (migration 00004 + sqlc) scoped to projects
- POST /api/v1/ingest — single or <=500-event batches via project prm_
  keys (X-Primora-Key alias), auto-registering components, permissive CORS
- Issues: fingerprint-grouped errors with count/severity/first+last seen
- Reads: events w/ filters + before-cursor, components, stats (1h/24h/7d/30d
  bucketed series + component health + totals + metric names), metric
  series (avg/p50/p95/max)
- GET .../telemetry/stream — project-scoped SSE through an in-process hub
  (buffered subs, drop-on-slow, 25s keepalives); ?api_key=/?token= auth
  for EventSource
- Telemetry page: health/issues/events/metrics/integrate tabs, live badge,
  390px-clean; demo stubs
- @primora/client SDK (createPrimoraClient: capture*/heartbeat/installAuto,
  queued keepalive flushes)
- MCP: primora_list_issues/list_events/telemetry_stats/metric_series
- IMS README deprecation note pointing here

Also fixes workspace pins to 0.3.0 — npm ci in the frontend image was
resolving the unpublished 0.2.0 spec from the registry.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 16:21:03 +02:00

51 lines
1004 B
Go

package services
import (
"encoding/json"
"sync"
)
// EventHub fans out ingested telemetry events to SSE subscribers, keyed by
// project ID. Slow consumers are dropped rather than back-pressured.
type EventHub struct {
mu sync.Mutex
subs map[string]map[chan []byte]struct{}
}
func NewEventHub() *EventHub {
return &EventHub{subs: map[string]map[chan []byte]struct{}{}}
}
func (h *EventHub) Subscribe(projectID string) chan []byte {
ch := make(chan []byte, 64)
h.mu.Lock()
if h.subs[projectID] == nil {
h.subs[projectID] = map[chan []byte]struct{}{}
}
h.subs[projectID][ch] = struct{}{}
h.mu.Unlock()
return ch
}
func (h *EventHub) Unsubscribe(projectID string, ch chan []byte) {
h.mu.Lock()
delete(h.subs[projectID], ch)
h.mu.Unlock()
close(ch)
}
func (h *EventHub) Broadcast(projectID string, v any) {
data, err := json.Marshal(v)
if err != nil {
return
}
h.mu.Lock()
for ch := range h.subs[projectID] {
select {
case ch <- data:
default:
}
}
h.mu.Unlock()
}