Skip to content

Commit ee0db4d

Browse files
authored
Dev/olivierdubo/browser metrics (#191)
* feat(browser_databases): Add metrics view on databases and analytics Signed-off-by: olivier dubo <olivier.dubo@ovhcloud.com> --------- Signed-off-by: olivier dubo <olivier.dubo@ovhcloud.com> Signed-off-by: Olivier <146172274+duboolivier@users.noreply.github.com>
1 parent 8b2b438 commit ee0db4d

4 files changed

Lines changed: 568 additions & 7 deletions

File tree

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.26.0
55
require (
66
code.cloudfoundry.org/bytefmt v0.64.0
77
dario.cat/mergo v1.0.2
8+
github.com/NimbleMarkets/ntcharts v0.5.1
89
github.com/PaesslerAG/gval v1.2.4
910
github.com/PaesslerAG/jsonpath v0.1.1
1011
github.com/amstuta/fx v0.0.0-20250411141758-99b16761e78b
@@ -48,6 +49,7 @@ require (
4849
github.com/gorilla/css v1.0.1 // indirect
4950
github.com/inconshreveable/mousetrap v1.1.0 // indirect
5051
github.com/josharian/intern v1.0.0 // indirect
52+
github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e // indirect
5153
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
5254
github.com/mailru/easyjson v0.9.1 // indirect
5355
github.com/mattn/go-isatty v0.0.20 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
44
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
55
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
66
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
7+
github.com/NimbleMarkets/ntcharts v0.5.1 h1:HWtekubEXfESwi24pyFynwGo2Hulbb9fPh7INMUc1dg=
8+
github.com/NimbleMarkets/ntcharts v0.5.1/go.mod h1:zVeRqYkh2n59YPe1bflaSL4O2aD2ZemNmrbdEqZ70hk=
79
github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
810
github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU=
911
github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
@@ -94,6 +96,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
9496
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
9597
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
9698
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
99+
github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e h1:OLwZ8xVaeVrru0xyeuOX+fne0gQTFEGlzfNjipCbxlU=
100+
github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e/go.mod h1:NQ34EGeu8FAYGBMDzwhfNJL8YQYoWZP5xYJPRDAwN3E=
97101
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
98102
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
99103
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=

internal/services/browser/managed_db_wizard.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"sort"
1414
"strconv"
1515
"strings"
16+
"time"
1617

1718
tea "github.com/charmbracelet/bubbletea"
1819
"github.com/charmbracelet/lipgloss"
@@ -251,6 +252,166 @@ func (m Model) createDBTopic(name string, minInsyncReplicas, partitions, replica
251252
}
252253
}
253254

255+
// ── Metrics ──────────────────────────────────────────────────────────────────
256+
257+
// dbMetricNamesMsg is sent after listing available metric names.
258+
type dbMetricNamesMsg struct {
259+
names []string
260+
err error
261+
}
262+
263+
// dbMetricDataMsg is sent after fetching one metric's time-series data.
264+
type dbMetricDataMsg struct {
265+
name string
266+
series []dbMetricSeries
267+
err error
268+
}
269+
270+
// dbMetricSeries holds one named time-series returned by the API (one per node/disk/etc.).
271+
type dbMetricSeries struct {
272+
Name string
273+
Points []dbMetricPoint
274+
}
275+
276+
type dbMetricPoint struct {
277+
T time.Time
278+
V float64
279+
}
280+
281+
// fetchDBMetricNames lists available metric names for the current service.
282+
func (m Model) fetchDBMetricNames() tea.Cmd {
283+
return func() tea.Msg {
284+
engine := getStringValue(m.detailData, "engine", "")
285+
serviceId := getStringValue(m.detailData, "id", "")
286+
if engine == "" || serviceId == "" {
287+
return dbMetricNamesMsg{err: fmt.Errorf("missing engine or service ID")}
288+
}
289+
endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s/metric",
290+
m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId))
291+
var names []string
292+
if err := httpLib.Client.Get(endpoint, &names); err != nil {
293+
return dbMetricNamesMsg{err: err}
294+
}
295+
sort.Strings(names)
296+
return dbMetricNamesMsg{names: names}
297+
}
298+
}
299+
300+
// dbNodeNamesMsg carries the ordered list of node names for the current service.
301+
type dbNodeNamesMsg struct {
302+
names []string
303+
err error
304+
}
305+
306+
// fetchDBNodeNames fetches the node list and returns names sorted alphabetically.
307+
func (m Model) fetchDBNodeNames() tea.Cmd {
308+
return func() tea.Msg {
309+
engine := getStringValue(m.detailData, "engine", "")
310+
serviceId := getStringValue(m.detailData, "id", "")
311+
if engine == "" || serviceId == "" {
312+
return dbNodeNamesMsg{err: fmt.Errorf("missing engine or service ID")}
313+
}
314+
endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s/node",
315+
m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId))
316+
var ids []string
317+
if err := httpLib.Client.Get(endpoint, &ids); err != nil {
318+
return dbNodeNamesMsg{err: err}
319+
}
320+
type nodeDetail struct {
321+
Name string `json:"name"`
322+
}
323+
var names []string
324+
for _, id := range ids {
325+
var nd nodeDetail
326+
detailEp := fmt.Sprintf("%s/%s", endpoint, url.PathEscape(id))
327+
if err := httpLib.Client.Get(detailEp, &nd); err == nil && nd.Name != "" {
328+
names = append(names, nd.Name)
329+
} else {
330+
names = append(names, id)
331+
}
332+
}
333+
sort.Strings(names)
334+
return dbNodeNamesMsg{names: names}
335+
}
336+
}
337+
338+
// fetchDBMetric fetches metric data for the given metric name and period.
339+
func (m Model) fetchDBMetric(metricName, period string) tea.Cmd {
340+
return func() tea.Msg {
341+
engine := getStringValue(m.detailData, "engine", "")
342+
serviceId := getStringValue(m.detailData, "id", "")
343+
if engine == "" || serviceId == "" {
344+
return dbMetricDataMsg{name: metricName, err: fmt.Errorf("missing engine or service ID")}
345+
}
346+
endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s/metric/%s?period=%s",
347+
m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId),
348+
url.PathEscape(metricName), url.QueryEscape(period))
349+
// Capture raw JSON so we can inspect the actual field names for debugging
350+
var rawResult json.RawMessage
351+
if err := httpLib.Client.Get(endpoint, &rawResult); err != nil {
352+
return dbMetricDataMsg{name: metricName, err: err}
353+
}
354+
var result struct {
355+
Metrics []struct {
356+
Name string `json:"name"`
357+
Host string `json:"host"`
358+
NodeID string `json:"nodeId"`
359+
Labels map[string]string `json:"labels"`
360+
Tags map[string]string `json:"tags"`
361+
DataPoints []struct {
362+
Timestamp float64 `json:"timestamp"`
363+
Value *float64 `json:"value"`
364+
} `json:"dataPoints"`
365+
} `json:"metrics"`
366+
}
367+
if err := json.Unmarshal(rawResult, &result); err != nil {
368+
return dbMetricDataMsg{name: metricName, err: err}
369+
}
370+
var series []dbMetricSeries
371+
for i, metric := range result.Metrics {
372+
// Try all possible label fields the API might provide
373+
sName := metric.Name
374+
if sName == "" {
375+
sName = metric.Host
376+
}
377+
if sName == "" {
378+
sName = metric.NodeID
379+
}
380+
if sName == "" && len(metric.Labels) > 0 {
381+
for k, v := range metric.Labels {
382+
sName = k + "=" + v
383+
break
384+
}
385+
}
386+
if sName == "" && len(metric.Tags) > 0 {
387+
for k, v := range metric.Tags {
388+
sName = k + "=" + v
389+
break
390+
}
391+
}
392+
if sName == "" {
393+
sName = fmt.Sprintf("node %d", i+1)
394+
}
395+
var pts []dbMetricPoint
396+
for _, dp := range metric.DataPoints {
397+
if dp.Value == nil || dp.Timestamp <= 0 {
398+
continue // skip null/gap entries
399+
}
400+
pts = append(pts, dbMetricPoint{
401+
T: time.Unix(int64(dp.Timestamp), 0),
402+
V: *dp.Value,
403+
})
404+
}
405+
if len(pts) == 0 {
406+
continue
407+
}
408+
sort.Slice(pts, func(i, j int) bool { return pts[i].T.Before(pts[j].T) })
409+
series = append(series, dbMetricSeries{Name: sName, Points: pts})
410+
}
411+
return dbMetricDataMsg{name: metricName, series: series}
412+
}
413+
}
414+
254415
// fetchDBACL fetches the ACL list for the current analytics service.
255416
func (m Model) fetchDBACL() tea.Cmd {
256417
return func() tea.Msg {

0 commit comments

Comments
 (0)