diff --git a/data/templates/report/report.css b/data/templates/report/report.css index 52a4b2f8..f29773f7 100644 --- a/data/templates/report/report.css +++ b/data/templates/report/report.css @@ -44,4 +44,84 @@ table { font-size: 8pt; } .closebtn:hover { color: black; +} + +/* Split-pane layout for etcd page */ +.etcd-panel-left { + width: 70%; + min-width: 70%; + padding-right: 12px; +} +.etcd-panel-divider { + width: 6px; + min-width: 6px; + cursor: col-resize; + background: #dee2e6; + transition: background 0.15s; +} +.etcd-panel-divider:hover { + background: #adb5bd; +} +.etcd-panel-right { + width: 30%; + min-width: 30%; + padding-left: 12px; +} +.etcd-chart-card { + background: #fff; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 16px; + margin-bottom: 16px; +} +.etcd-chart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} +.etcd-chart-card h6 { + margin: 0; + color: #495057; + font-size: 0.85rem; + font-weight: 600; +} +.etcd-chart-toolbar { + display: flex; + gap: 4px; +} +.etcd-chart-btn { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 2px 8px; + font-size: 0.75rem; + cursor: pointer; + color: #495057; + line-height: 1.4; +} +.etcd-chart-btn:hover { + background: #e9ecef; +} +.etcd-chart-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.6); + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; +} +.etcd-chart-overlay-content { + background: #fff; + border-radius: 8px; + padding: 20px; + width: 90vw; + max-height: 90vh; +} +.etcd-chart-overlay-header { + display: flex; + justify-content: flex-end; + gap: 4px; + margin-bottom: 8px; } \ No newline at end of file diff --git a/data/templates/report/report.html b/data/templates/report/report.html index 0bd93b9c..2730bc6e 100644 --- a/data/templates/report/report.html +++ b/data/templates/report/report.html @@ -10,6 +10,10 @@ + + + + @@ -21,12 +25,11 @@
Redirecting to metrics dashboard...
+diff --git a/go.mod b/go.mod index 444dbc33..cb6bd048 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,6 @@ require ( require ( github.com/aws/aws-sdk-go v1.55.6 github.com/evanphx/json-patch v4.12.0+incompatible - github.com/go-echarts/go-echarts/v2 v2.5.1 github.com/google/go-cmp v0.7.0 github.com/hashicorp/go-retryablehttp v0.7.7 github.com/jedib0t/go-pretty/v6 v6.6.7 diff --git a/go.sum b/go.sum index 72c611e1..bb22253c 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,6 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-echarts/go-echarts/v2 v2.5.1 h1:kFVNaS3IsszKOQmUyCi95D2IhipE5twfvaBhFLOfPrs= -github.com/go-echarts/go-echarts/v2 v2.5.1/go.mod h1:56YlvzhW/a+du15f3S2qUGNDfKnFOeJSThBIrVFHDtI= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/internal/opct/summary/result.go b/internal/opct/summary/result.go index 58fc305c..6b23de52 100644 --- a/internal/opct/summary/result.go +++ b/internal/opct/summary/result.go @@ -512,15 +512,15 @@ func (rs *ResultSummary) extractAndLoadData() error { log.Error("Processing results/Populating/Populating Summary/Processing/CAMGI: Not Found") } if len(MetricsData.Bytes()) > 0 { - rs.Metrics, err = mustgathermetrics.NewMustGatherMetrics(rs.SavePath+"/metrics", pathMetrics, "/metrics", &MetricsData) + rs.Metrics, err = mustgathermetrics.NewMustGatherMetrics(rs.SavePath+"/metrics", &MetricsData) if err != nil { log.Errorf("Processing results/Populating/Populating Summary/Processing/MetricsData: %v", err) } else { - err := rs.Metrics.Process() - if err != nil { + if err := rs.Metrics.Process(); err != nil { log.Errorf("Processing MetricsData: %v", err) + } else { + rs.HasMetrics = true } - rs.HasMetrics = true } } else { log.Error("Processing results/Populating/Populating Summary/Processing/MetricsData: Not Found") diff --git a/internal/openshift/mustgathermetrics/charts-config.json b/internal/openshift/mustgathermetrics/charts-config.json new file mode 100644 index 00000000..fe882c7c --- /dev/null +++ b/internal/openshift/mustgathermetrics/charts-config.json @@ -0,0 +1,40 @@ +{ + "charts": [ + { + "file": "query_range-etcd-disk-fsync-db-duration-p99.json.gz", + "label": "instance", + "title": "etcd fsync DB p99", + "id": "id1" + }, + { + "file": "query_range-api-kas-request-duration-p99.json.gz", + "label": "verb", + "title": "Kube API request p99", + "id": "id2" + }, + { + "file": "query_range-etcd-disk-fsync-wal-duration-p99.json.gz", + "label": "instance", + "title": "etcd fsync WAL p99", + "id": "id0" + }, + { + "file": "query_range-etcd-peer-round-trip-time.json.gz", + "label": "instance", + "title": "etcd peer round trip", + "id": "id3" + }, + { + "file": "query_range-etcd-total-leader-elections-day.json.gz", + "label": "instance", + "title": "etcd peer total leader election", + "id": "id4" + }, + { + "file": "query_range-etcd-request-duration-p99.json.gz", + "label": "operation", + "title": "etcd req duration p99", + "id": "id5" + } + ] +} diff --git a/internal/openshift/mustgathermetrics/charts.go b/internal/openshift/mustgathermetrics/charts.go deleted file mode 100644 index 1711079a..00000000 --- a/internal/openshift/mustgathermetrics/charts.go +++ /dev/null @@ -1,198 +0,0 @@ -package mustgathermetrics - -import ( - "encoding/json" - "fmt" - "io" - "os" - "time" - - "github.com/go-echarts/go-echarts/v2/charts" - "github.com/go-echarts/go-echarts/v2/components" - "github.com/go-echarts/go-echarts/v2/opts" - log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" -) - -type MetricValue struct { - Timestap time.Time - Value string -} - -type PrometheusResultMetric struct { - Metric map[string]string `json:"metric"` - Values [][]interface{} `json:"values"` -} - -type PrometheusResponse struct { - Status string `json:"status"` - Data struct { - ResultType string `json:"resultType"` - Result []PrometheusResultMetric `json:"result"` - } `json:"data"` -} - -type readMetricInput struct { - filename string - label string - title string - subtitle string -} - -// newMetricsPage create the page object to genera the metric report. -func newMetricsPage() *components.Page { - page := components.NewPage() - page.PageTitle = "OPCT Report Metrics" - return page -} - -// SaveMetricsPageReport Create HTML metrics file in a given path. -func SaveMetricsPageReport(page *components.Page, path string) error { - - f, err := os.Create(path) - if err != nil { - return err - } - if err := page.Render(io.MultiWriter(f)); err != nil { - return err - } - return nil -} - -func (mmm *MustGatherChart) NewChart() *charts.Line { - return mmm.processMetric(&readMetricInput{ - filename: mmm.Path, - label: mmm.PlotLabel, - title: mmm.PlotTitle, - subtitle: mmm.PlotSubTitle, - }) -} - -func (mmm *MustGatherChart) NewCharts() []*charts.Line { - in := &readMetricInput{ - filename: mmm.Path, - label: mmm.PlotLabel, - title: mmm.PlotTitle, - subtitle: mmm.PlotSubTitle, - } - return mmm.processMetrics(in) -} - -// LoadData generates the metric widget (plot graph from data series). -func (mmm *MustGatherChart) LoadData(payload []byte) error { - mmm.MetricData = &PrometheusResponse{} - - err := json.Unmarshal(payload, &mmm.MetricData) - if err != nil { - log.Errorf("Metrics/Extractor/Processing/LoadMetric ERROR parsing metric data: %v", err) - return err - } - log.Debugf("Metrics/Extractor/Processing/LoadMetric Status: %s\n", mmm.MetricData.Status) - return nil -} - -// processMetric generates the metric widget (plot graph from data series). -func (mmm *MustGatherChart) processMetric(in *readMetricInput) *charts.Line { - - line := charts.NewLine() - line.SetGlobalOptions( - charts.WithTitleOpts(opts.Title{ - Title: in.title, - Subtitle: in.subtitle, - }), - charts.WithTooltipOpts(opts.Tooltip{Show: ptr.To(true), Trigger: "axis"}), - ) - - allTimestamps := []string{} - - type ChartData struct { - Label string - DataPoints []opts.LineData - } - - chartData := []ChartData{} - idx := 0 - for _, res := range mmm.MetricData.Data.Result { - chart := ChartData{ - Label: res.Metric[in.label], - DataPoints: make([]opts.LineData, 0), - } - for _, datapoints := range res.Values { - value := datapoints[1].(string) - if value == "" { - log.Debugf("Metrics/Extractor/Processing/GenChart: Empty value [%s], ignoring...", value) - continue - } - // Convert from Unix timestamp to string value - tm := time.Unix(int64(datapoints[0].(float64)), 0) - strTimestamp := fmt.Sprintf("%d-%d-%d %d:%d:%d", tm.Year(), tm.Month(), tm.Day(), tm.Hour(), tm.Minute(), tm.Second()) - - allTimestamps = append(allTimestamps, strTimestamp) - chart.DataPoints = append(chart.DataPoints, opts.LineData{ - Value: value, - XAxisIndex: idx, - }) - idx += 1 - } - chartData = append(chartData, chart) - } - - // sort.Strings(allTimestamps) - line.SetXAxis(allTimestamps). - SetSeriesOptions(charts.WithLineChartOpts( - opts.LineChart{Smooth: ptr.To(false), ShowSymbol: ptr.To(true), SymbolSize: 15, Symbol: "diamond"}, - )) - for _, chart := range chartData { - line.AddSeries(chart.Label, chart.DataPoints) - } - - return line -} - -// processMetric generates the metric widget (plot graph from data series). -func (mmm *MustGatherChart) processMetrics(in *readMetricInput) []*charts.Line { - - var lines []*charts.Line - idx := 0 - for _, res := range mmm.MetricData.Data.Result { - allTimestamps := []string{} - line := charts.NewLine() - line.SetGlobalOptions( - charts.WithTitleOpts(opts.Title{ - Title: in.title, - Subtitle: in.subtitle, - }), - charts.WithTooltipOpts(opts.Tooltip{Show: ptr.To(true), Trigger: "axis"}), - ) - dataPoints := make([]opts.LineData, 0) - for _, datapoints := range res.Values { - value := datapoints[1].(string) - if value == "" { - log.Debugf("Metrics/Extractor/Processing/GenChart: Empty value [%s], ignoring...", value) - continue - } - // Convert from Unix timestamp to string value - tm := time.Unix(int64(datapoints[0].(float64)), 0) - strTimestamp := fmt.Sprintf("%d-%d-%d %d:%d:%d", tm.Year(), tm.Month(), tm.Day(), tm.Hour(), tm.Minute(), tm.Second()) - - allTimestamps = append(allTimestamps, strTimestamp) - dataPoints = append(dataPoints, opts.LineData{ - Value: value, - XAxisIndex: idx, - }) - idx += 1 - } - line.SetXAxis(allTimestamps). - SetSeriesOptions(charts.WithLineChartOpts( - opts.LineChart{Smooth: ptr.To(false), ShowSymbol: ptr.To(true), SymbolSize: 15, Symbol: "diamond"}, - )) - line.AddSeries(res.Metric[in.label], dataPoints) - lines = append(lines, line) - } - - // sort.Strings(allTimestamps) - // line.SetSeriesOptions(charts.WithLineChartOpts( - // opts.LineChart{Smooth: false, ShowSymbol: true, SymbolSize: 15, Symbol: "diamond"}, - // )) - return lines -} diff --git a/internal/openshift/mustgathermetrics/index.html b/internal/openshift/mustgathermetrics/index.html new file mode 100644 index 00000000..d1ebe625 --- /dev/null +++ b/internal/openshift/mustgathermetrics/index.html @@ -0,0 +1,10 @@ + + +
+ +
+ +
+