T4.1: Real-time Metrics Dashboard - Add internal/dashboard package with MetricsAggregator - Record, aggregate, and query metrics - Percentile calculations (p50, p95, p99) - Time-series data with max size eviction - 13 metrics tests, all passing T4.2: Workflow Visualization & DAG Rendering - Add internal/visualization package with DAGRenderer - Convert dependency graphs to DOT format - Critical path highlighting - Topological sorting with parallel task detection - HTML rendering for visualization - 11 DAG rendering tests, all passing T4.3: Advanced Search & Filtering - Add internal/search package with WorkflowSearch - Full-text indexing with word-based lookup - Filter by status, assignee, tag, date range - Regex pattern matching - Saved filters for reusable queries - 15 search tests, all passing T4.4: Cost Tracking & Optimization - Add internal/cost package with CostTracker - Track LLM API costs (by token) - Track git operation costs - Track compute resource costs (by duration) - Cost aggregation by workflow/type - Optimization recommendations - 11 cost tests, all passing Total T4.1-T4.4: 50 tests passing Next: T4.5-T4.8 (alerting, profiling, multi-cluster, self-deployment)
204 lines
4.4 KiB
Go
204 lines
4.4 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// MetricSnapshot represents a point-in-time metric value
|
|
type MetricSnapshot struct {
|
|
Timestamp time.Time
|
|
Value float64
|
|
Name string
|
|
}
|
|
|
|
// MetricsAggregator aggregates Prometheus metrics for dashboard display
|
|
type MetricsAggregator struct {
|
|
mu sync.RWMutex
|
|
metrics map[string][]MetricSnapshot
|
|
ttl time.Duration
|
|
maxSize int
|
|
}
|
|
|
|
// NewMetricsAggregator creates a new metrics aggregator
|
|
func NewMetricsAggregator(ttl time.Duration, maxSize int) *MetricsAggregator {
|
|
return &MetricsAggregator{
|
|
metrics: make(map[string][]MetricSnapshot),
|
|
ttl: ttl,
|
|
maxSize: maxSize,
|
|
}
|
|
}
|
|
|
|
// Record records a metric value
|
|
func (ma *MetricsAggregator) Record(name string, value float64) {
|
|
ma.mu.Lock()
|
|
defer ma.mu.Unlock()
|
|
|
|
snapshot := MetricSnapshot{
|
|
Timestamp: time.Now(),
|
|
Value: value,
|
|
Name: name,
|
|
}
|
|
|
|
ma.metrics[name] = append(ma.metrics[name], snapshot)
|
|
|
|
// Trim old entries
|
|
if len(ma.metrics[name]) > ma.maxSize {
|
|
ma.metrics[name] = ma.metrics[name][1:]
|
|
}
|
|
}
|
|
|
|
// GetTimeSeries retrieves metric time series
|
|
func (ma *MetricsAggregator) GetTimeSeries(name string) []MetricSnapshot {
|
|
ma.mu.RLock()
|
|
defer ma.mu.RUnlock()
|
|
|
|
snapshots, exists := ma.metrics[name]
|
|
if !exists {
|
|
return []MetricSnapshot{}
|
|
}
|
|
|
|
result := make([]MetricSnapshot, len(snapshots))
|
|
copy(result, snapshots)
|
|
return result
|
|
}
|
|
|
|
// GetPercentile calculates percentile for a metric
|
|
func (ma *MetricsAggregator) GetPercentile(name string, percentile float64) (float64, error) {
|
|
ma.mu.RLock()
|
|
defer ma.mu.RUnlock()
|
|
|
|
snapshots, exists := ma.metrics[name]
|
|
if !exists || len(snapshots) == 0 {
|
|
return 0, fmt.Errorf("metric not found: %s", name)
|
|
}
|
|
|
|
values := make([]float64, len(snapshots))
|
|
for i, s := range snapshots {
|
|
values[i] = s.Value
|
|
}
|
|
|
|
sort.Float64s(values)
|
|
|
|
index := int(float64(len(values)) * percentile / 100)
|
|
if index >= len(values) {
|
|
index = len(values) - 1
|
|
}
|
|
|
|
return values[index], nil
|
|
}
|
|
|
|
// GetAverage calculates average for a metric
|
|
func (ma *MetricsAggregator) GetAverage(name string) (float64, error) {
|
|
ma.mu.RLock()
|
|
defer ma.mu.RUnlock()
|
|
|
|
snapshots, exists := ma.metrics[name]
|
|
if !exists || len(snapshots) == 0 {
|
|
return 0, fmt.Errorf("metric not found: %s", name)
|
|
}
|
|
|
|
sum := 0.0
|
|
for _, s := range snapshots {
|
|
sum += s.Value
|
|
}
|
|
|
|
return sum / float64(len(snapshots)), nil
|
|
}
|
|
|
|
// GetMax returns maximum value for a metric
|
|
func (ma *MetricsAggregator) GetMax(name string) (float64, error) {
|
|
ma.mu.RLock()
|
|
defer ma.mu.RUnlock()
|
|
|
|
snapshots, exists := ma.metrics[name]
|
|
if !exists || len(snapshots) == 0 {
|
|
return 0, fmt.Errorf("metric not found: %s", name)
|
|
}
|
|
|
|
max := snapshots[0].Value
|
|
for _, s := range snapshots {
|
|
if s.Value > max {
|
|
max = s.Value
|
|
}
|
|
}
|
|
|
|
return max, nil
|
|
}
|
|
|
|
// GetMin returns minimum value for a metric
|
|
func (ma *MetricsAggregator) GetMin(name string) (float64, error) {
|
|
ma.mu.RLock()
|
|
defer ma.mu.RUnlock()
|
|
|
|
snapshots, exists := ma.metrics[name]
|
|
if !exists || len(snapshots) == 0 {
|
|
return 0, fmt.Errorf("metric not found: %s", name)
|
|
}
|
|
|
|
min := snapshots[0].Value
|
|
for _, s := range snapshots {
|
|
if s.Value < min {
|
|
min = s.Value
|
|
}
|
|
}
|
|
|
|
return min, nil
|
|
}
|
|
|
|
// GetMetricNames returns all recorded metric names
|
|
func (ma *MetricsAggregator) GetMetricNames() []string {
|
|
ma.mu.RLock()
|
|
defer ma.mu.RUnlock()
|
|
|
|
names := make([]string, 0, len(ma.metrics))
|
|
for name := range ma.metrics {
|
|
names = append(names, name)
|
|
}
|
|
|
|
return names
|
|
}
|
|
|
|
// GetLatest returns the latest snapshot for a metric
|
|
func (ma *MetricsAggregator) GetLatest(name string) (MetricSnapshot, error) {
|
|
ma.mu.RLock()
|
|
defer ma.mu.RUnlock()
|
|
|
|
snapshots, exists := ma.metrics[name]
|
|
if !exists || len(snapshots) == 0 {
|
|
return MetricSnapshot{}, fmt.Errorf("metric not found: %s", name)
|
|
}
|
|
|
|
return snapshots[len(snapshots)-1], nil
|
|
}
|
|
|
|
// Clear clears all metrics
|
|
func (ma *MetricsAggregator) Clear() {
|
|
ma.mu.Lock()
|
|
defer ma.mu.Unlock()
|
|
|
|
ma.metrics = make(map[string][]MetricSnapshot)
|
|
}
|
|
|
|
// GetCountInRange returns count of metrics within a time range
|
|
func (ma *MetricsAggregator) GetCountInRange(name string, start, end time.Time) (int, error) {
|
|
ma.mu.RLock()
|
|
defer ma.mu.RUnlock()
|
|
|
|
snapshots, exists := ma.metrics[name]
|
|
if !exists {
|
|
return 0, fmt.Errorf("metric not found: %s", name)
|
|
}
|
|
|
|
count := 0
|
|
for _, s := range snapshots {
|
|
if s.Timestamp.After(start) && s.Timestamp.Before(end) {
|
|
count++
|
|
}
|
|
}
|
|
|
|
return count, nil
|
|
}
|