better handling

This commit is contained in:
2026-04-05 09:47:25 +02:00
parent 0fd17e4944
commit 988ea1d919
5 changed files with 357 additions and 282 deletions
+12 -2
View File
@@ -98,7 +98,11 @@ func (c *Collector) Subscribe(filter func(websocket.HistoryEntry) bool) (chan we
return ch, cleanup
}
func (c *Collector) GetHistory() []websocket.HistoryEntry {
func (c *Collector) GetHistory(limit int) []websocket.HistoryEntry {
if limit <= 0 {
limit = c.historySize
}
c.historyMutex.RLock()
defer c.historyMutex.RUnlock()
@@ -108,6 +112,12 @@ func (c *Collector) GetHistory() []websocket.HistoryEntry {
history = append(history, entry)
}
})
// Return only the last 'limit' items if we have more
if len(history) > limit {
history = history[len(history)-limit:]
}
return history
}
@@ -121,7 +131,7 @@ func (c *Collector) SearchHistory(ctx context.Context, query string, limit int)
}
// Fallback to in-memory if no store
history := c.GetHistory()
history := c.GetHistory(limit)
var filtered []websocket.HistoryEntry
// Lowercase query for case-insensitive search
lowerQuery := strings.ToLower(query)
+30
View File
@@ -50,3 +50,33 @@ OK:
t.Errorf("expected type 'logs', got %s", store.entries[0].Type)
}
}
func TestCollectorHistoryLimit(t *testing.T) {
coll := New(nil, 10, nil)
for i := 0; i < 10; i++ {
coll.addToHistory(websocket.DataTypeLogs, map[string]interface{}{"msg": i})
}
history := coll.GetHistory(10)
if len(history) != 10 {
t.Errorf("expected 10 entries, got %d", len(history))
}
history = coll.GetHistory(5)
if len(history) != 5 {
t.Errorf("expected 5 entries, got %d", len(history))
}
// Should default to historySize (10)
history = coll.GetHistory(0)
if len(history) != 10 {
t.Errorf("expected default limit to be historySize (10), got %d", len(history))
}
// Should not crash on large limit, just return what it has
history = coll.GetHistory(100)
if len(history) != 10 {
t.Errorf("expected 10 entries even with limit 100, got %d", len(history))
}
}
+290
View File
@@ -0,0 +1,290 @@
package collector
import (
"context"
"encoding/json"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
"git.tornberg.me/go-otel/internal/websocket"
"go.opentelemetry.io/collector/pdata/plog"
"go.opentelemetry.io/collector/pdata/pmetric"
"go.opentelemetry.io/collector/pdata/ptrace"
)
func (c *Collector) HandleTraces(w http.ResponseWriter, r *http.Request) {
log.Printf("Received traces request: method=%s, content-type=%s", r.Method, r.Header.Get("Content-Type"))
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("failed to read request body: %v", err)
http.Error(w, "failed to read request body", http.StatusInternalServerError)
return
}
log.Printf("Received traces body, size=%d", len(body))
var unmarshaler ptrace.Unmarshaler
contentType := r.Header.Get("Content-Type")
switch contentType {
case "application/x-protobuf":
unmarshaler = &ptrace.ProtoUnmarshaler{}
case "application/json":
unmarshaler = &ptrace.JSONUnmarshaler{}
default:
unmarshaler = &ptrace.JSONUnmarshaler{} // default to JSON
}
traces, err := unmarshaler.UnmarshalTraces(body)
if err != nil {
log.Printf("failed to unmarshal traces: %v", err)
http.Error(w, "failed to unmarshal traces", http.StatusBadRequest)
return
}
log.Printf("Successfully unmarshaled traces, span_count=%d", traces.SpanCount())
if err := c.ConsumeTraces(context.Background(), traces); err != nil {
log.Printf("failed to consume traces: %v", err)
http.Error(w, "failed to consume traces", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (c *Collector) HandleMetrics(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("failed to read request body: %v", err)
http.Error(w, "failed to read request body", http.StatusInternalServerError)
return
}
var unmarshaler pmetric.Unmarshaler
contentType := r.Header.Get("Content-Type")
switch contentType {
case "application/x-protobuf":
unmarshaler = &pmetric.ProtoUnmarshaler{}
case "application/json":
unmarshaler = &pmetric.JSONUnmarshaler{}
default:
unmarshaler = &pmetric.JSONUnmarshaler{} // default to JSON
}
metrics, err := unmarshaler.UnmarshalMetrics(body)
if err != nil {
log.Printf("failed to unmarshal metrics: %v", err)
http.Error(w, "failed to unmarshal metrics", http.StatusBadRequest)
return
}
if err := c.ConsumeMetrics(context.Background(), metrics); err != nil {
log.Printf("failed to consume metrics: %v", err)
http.Error(w, "failed to consume metrics", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (c *Collector) HandleLogs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("failed to read request body: %v", err)
http.Error(w, "failed to read request body", http.StatusInternalServerError)
return
}
var unmarshaler plog.Unmarshaler
contentType := r.Header.Get("Content-Type")
switch contentType {
case "application/x-protobuf":
unmarshaler = &plog.ProtoUnmarshaler{}
case "application/json":
unmarshaler = &plog.JSONUnmarshaler{}
default:
unmarshaler = &plog.JSONUnmarshaler{} // default to JSON
}
logs, err := unmarshaler.UnmarshalLogs(body)
if err != nil {
log.Printf("failed to unmarshal logs: %v", err)
http.Error(w, "failed to unmarshal logs", http.StatusBadRequest)
return
}
if err := c.ConsumeLogs(context.Background(), logs); err != nil {
log.Printf("failed to consume logs: %v", err)
http.Error(w, "failed to consume logs", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (c *Collector) HandleHistory(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
limitStr := r.URL.Query().Get("limit")
limit := 100
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil {
limit = l
}
}
history := c.GetHistory(limit)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(history); err != nil {
log.Printf("failed to encode history: %v", err)
http.Error(w, "failed to encode history", http.StatusInternalServerError)
return
}
}
func (c *Collector) HandleSearch(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
query := r.URL.Query().Get("q")
limitStr := r.URL.Query().Get("limit")
limit := 100
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil {
limit = l
}
}
history, err := c.SearchHistory(r.Context(), query, limit)
if err != nil {
log.Printf("failed to search history: %v", err)
http.Error(w, "failed to search history", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(history); err != nil {
log.Printf("failed to encode search results: %v", err)
http.Error(w, "failed to encode history", http.StatusInternalServerError)
return
}
}
func (c *Collector) HandleWait(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
query := r.URL.Query().Get("q")
timeoutSec := 30
if t := r.URL.Query().Get("timeout"); t != "" {
if ts, err := strconv.Atoi(t); err == nil {
timeoutSec = ts
}
}
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeoutSec)*time.Second)
defer cancel()
// First, check if result already exists in recent history
matches, err := c.SearchHistory(ctx, query, 1)
if err == nil && len(matches) > 0 {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(matches[0])
return
}
// Otherwise, subscribe and wait
lowerQuery := strings.ToLower(query)
ch, cleanup := c.Subscribe(func(entry websocket.HistoryEntry) bool {
if query == "" {
return true
}
data, _ := json.Marshal(entry.Data)
return strings.Contains(strings.ToLower(string(data)), lowerQuery)
})
defer cleanup()
select {
case entry := <-ch:
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(entry); err != nil {
log.Printf("failed to encode signal: %v", err)
}
case <-ctx.Done():
http.Error(w, "timeout waiting for signal", http.StatusGatewayTimeout)
}
}
func (c *Collector) HandleHistoryByType(dataType string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
limitStr := r.URL.Query().Get("limit")
limit := 100
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil {
limit = l
}
}
// Fetch a larger set to allow for filtering, then slice
// Retrieve up to 1000 from memory then filter
history := c.GetHistory(5000)
// Filter by type
var filteredHistory []websocket.HistoryEntry
for _, entry := range history {
if string(entry.Type) == dataType {
filteredHistory = append(filteredHistory, entry)
}
}
// Limit the results
if len(filteredHistory) > limit {
filteredHistory = filteredHistory[len(filteredHistory)-limit:]
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(filteredHistory); err != nil {
log.Printf("failed to encode filtered history: %v", err)
http.Error(w, "failed to encode history", http.StatusInternalServerError)
return
}
}
}
func (c *Collector) HandleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
+4 -3
View File
@@ -18,7 +18,7 @@ const (
)
type HistoryProvider interface {
GetHistory() []HistoryEntry
GetHistory(limit int) []HistoryEntry
}
type HistoryEntry struct {
@@ -72,7 +72,7 @@ func (s *Server) HandleConnections(w http.ResponseWriter, r *http.Request) {
// // Send history to new client
// if s.historyProvider != nil {
// history := s.historyProvider.GetHistory()
// history := s.historyProvider.GetHistory(50)
// for _, entry := range history {
// if data, err := json.Marshal(entry); err == nil {
// if err := ws.WriteMessage(websocket.TextMessage, data); err != nil {
@@ -117,7 +117,8 @@ func (s *Server) SendHistory(ws *websocket.Conn) {
s.mutex.Lock()
defer s.mutex.Unlock()
history := s.historyProvider.GetHistory() // Assuming GetHistory() returns the history data
// Default to 100 entries for new socket connections if not otherwise specified
history := s.historyProvider.GetHistory(100)
for _, entry := range history {
data, err := json.Marshal(entry)
if err != nil {