Files
stats-gateway/internal/service/service.go
T

90 lines
2.4 KiB
Go

package service
import (
"context"
"time"
statsv1 "github.com/bergabruh/stats-gateway/gen/stats/v1"
"github.com/bergabruh/stats-gateway/internal/storage"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Service implements statsv1.SpeedStatusServer with public RecentResults
// and Aggregate methods. Tunnel returns Unimplemented until Task 11 wires
// auth + handler.
type Service struct {
statsv1.UnimplementedSpeedStatusServer
store *storage.Store
}
func New(st *storage.Store) *Service { return &Service{store: st} }
func (s *Service) RecentResults(f *statsv1.Filter, stream statsv1.SpeedStatus_RecentResultsServer) error {
limit := int(f.Limit)
if limit <= 0 || limit > 1000 {
limit = 100
}
var since time.Time
if f.Since != "" {
t, err := time.Parse(time.RFC3339, f.Since)
if err != nil {
return status.Errorf(codes.InvalidArgument, "since: %v", err)
}
since = t
}
samples, err := s.store.QueryRecent(stream.Context(), f.Node, limit, since)
if err != nil {
return status.Errorf(codes.Internal, "query: %v", err)
}
for _, r := range samples {
err := stream.Send(&statsv1.Sample{
Node: r.Node,
Timestamp: r.Timestamp.Format(time.RFC3339Nano),
HttpLatencyMs: r.HTTPLatencyMs,
TcpRttMs: r.TCPRttMs,
DnsResolveMs: r.DNSResolveMs,
TlsHandshakeMs: r.TLSHandshakeMs,
ThroughputKbps: r.ThroughputKbps,
})
if err != nil {
return err
}
}
return nil
}
// aggregateBounds defines bucket upper bounds (ms) used by Aggregate.
var aggregateBounds = []float64{25, 50, 100, 250, 500, 1000, 5000}
func (s *Service) Aggregate(ctx context.Context, r *statsv1.Range) (*statsv1.Histogram, error) {
since, _ := time.Parse(time.RFC3339, r.Since)
until, _ := time.Parse(time.RFC3339, r.Until)
if until.IsZero() {
until = time.Now().UTC()
}
if since.IsZero() {
return nil, status.Error(codes.InvalidArgument, "since required")
}
samples, err := s.store.QueryRange(ctx, r.Node, since, until)
if err != nil {
return nil, status.Errorf(codes.Internal, "query: %v", err)
}
counts := make([]int64, len(aggregateBounds))
for _, sm := range samples {
for i, b := range aggregateBounds {
if sm.HTTPLatencyMs <= b {
counts[i]++
break
}
}
}
h := &statsv1.Histogram{}
for i, b := range aggregateBounds {
h.Buckets = append(h.Buckets, &statsv1.Bucket{UpperMs: b, Count: counts[i]})
}
return h, nil
}