feat(T4.1-T4.4): implement advanced operations & analytics (part 1)
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)
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user