Skip to content

Commit 9ee7331

Browse files
committed
feat(browser_data): added logs in managed db
Signed-off-by: olivier dubo <olivier.dubo@ovhcloud.com>
1 parent 656bcb9 commit 9ee7331

2 files changed

Lines changed: 155 additions & 3 deletions

File tree

internal/services/browser/managed_db_wizard.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,30 @@ type dbPoolCreatedMsg struct {
149149
err error
150150
}
151151

152+
// dbLogsMsg is sent after fetching service log entries.
153+
type dbLogsMsg struct {
154+
logs []map[string]interface{}
155+
err error
156+
}
157+
158+
// fetchDBLogs fetches the most recent log entries for the current DB service.
159+
func (m Model) fetchDBLogs() tea.Cmd {
160+
return func() tea.Msg {
161+
engine := getStringValue(m.detailData, "engine", "")
162+
serviceId := getStringValue(m.detailData, "id", "")
163+
if engine == "" || serviceId == "" {
164+
return dbLogsMsg{err: fmt.Errorf("missing engine or service ID")}
165+
}
166+
endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s/logs",
167+
m.cloudProject, url.PathEscape(engine), url.PathEscape(serviceId))
168+
var logs []map[string]interface{}
169+
if err := httpLib.Client.Get(endpoint, &logs); err != nil {
170+
return dbLogsMsg{err: err}
171+
}
172+
return dbLogsMsg{logs: logs}
173+
}
174+
}
175+
152176
// createDBDatabase POSTs to the database/database endpoint to create a new logical database.
153177
func (m Model) createDBDatabase(name string) tea.Cmd {
154178
return func() tea.Msg {

internal/services/browser/manager.go

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,10 @@ type Model struct {
806806
dbDetailDatabases []map[string]interface{}
807807
dbDetailPools []map[string]interface{}
808808
dbDetailLoaded bool // true once fetchDBDetailSubresources has returned
809-
dbDetailTab int // 0=Service, 1=Users, 2=Backups, 3=Databases, 4=Pools
809+
dbDetailTab int // 0=Service, 1=Users, 2=Backups, 3=Databases, 4=Pools, 5=Logs
810+
dbDetailLogs []map[string]interface{} // last fetched log entries
811+
dbLogsLoaded bool // true once fetchDBLogs has returned
812+
dbLogsScrollOffset int // scroll offset for logs tab (0 = bottom/newest)
810813
// DB user state (Users tab)
811814
dbUserCreateMode bool // true when typing a new username
812815
dbUserCreateInput string // username being typed
@@ -954,6 +957,9 @@ type setDefaultProjectMsg struct {
954957
// clearNotificationMsg is sent to clear the notification after timeout
955958
type clearNotificationMsg struct{}
956959

960+
// dbLogsRefreshTickMsg triggers a periodic re-fetch of DB logs
961+
type dbLogsRefreshTickMsg struct{}
962+
957963
// refreshTickMsg is sent to trigger automatic refresh of data
958964
type refreshTickMsg struct{}
959965

@@ -2666,6 +2672,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
26662672
m.dbDBCreateMode = false
26672673
m.dbDBCreateInput = ""
26682674
m.dbPoolCreateStep = -1
2675+
m.dbDetailLogs = nil
2676+
m.dbLogsLoaded = false
2677+
m.dbLogsScrollOffset = 0
26692678
m.mode = LoadingView
26702679
path := "/databases"
26712680
if m.currentProduct == ProductManagedAnalytics {
@@ -2766,6 +2775,28 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
27662775
}
27672776
return m, tea.Tick(4*time.Second, func(t time.Time) tea.Msg { return clearNotificationMsg{} })
27682777

2778+
case dbLogsMsg:
2779+
m.dbLogsLoaded = true
2780+
if msg.err != nil {
2781+
m.dbDetailLogs = nil
2782+
m.notification = fmt.Sprintf("❌ Failed to fetch logs: %s", msg.err.Error())
2783+
m.notificationExpiry = time.Now().Add(8 * time.Second)
2784+
return m, tea.Tick(8*time.Second, func(t time.Time) tea.Msg { return clearNotificationMsg{} })
2785+
}
2786+
m.dbDetailLogs = msg.logs
2787+
// Schedule next auto-refresh in 10 s
2788+
return m, tea.Tick(10*time.Second, func(t time.Time) tea.Msg { return dbLogsRefreshTickMsg{} })
2789+
2790+
case dbLogsRefreshTickMsg:
2791+
// Only re-fetch if still viewing the Logs tab
2792+
if m.mode == DetailView &&
2793+
(m.currentProduct == ProductManagedDatabases || m.currentProduct == ProductManagedAnalytics) &&
2794+
m.dbDetailTab == 5 {
2795+
return m, m.fetchDBLogs()
2796+
}
2797+
// Tab was left — stop the loop
2798+
return m, nil
2799+
27692800
case dbCreatedMsg:
27702801
m.wizard.isLoading = false
27712802
m.wizard.loadingMessage = ""
@@ -7250,7 +7281,7 @@ func (m Model) renderManagedDatabaseDetail(width int) string {
72507281
Foreground(lipgloss.Color("#444444")).
72517282
Padding(0, 2)
72527283

7253-
tabNames := []string{"Service", "Users", "Backups", "Databases", "Pools"}
7284+
tabNames := []string{"Service", "Users", "Backups", "Databases", "Pools", "Logs"}
72547285
var tabParts []string
72557286
for i, name := range tabNames {
72567287
if i == 4 && !isPostgres {
@@ -7690,6 +7721,66 @@ func (m Model) renderManagedDatabaseDetail(width int) string {
76907721
}
76917722
content.WriteString(renderBox(fmt.Sprintf("Connection Pools (%d)", len(m.dbDetailPools)), strings.TrimRight(poolsContent.String(), "\n"), fullWidth))
76927723
}
7724+
7725+
case 5: // ── Logs ───────────────────────────────────────────────────
7726+
refreshBtn := lipgloss.NewStyle().
7727+
Foreground(lipgloss.Color("#7B68EE")).Bold(true).Padding(0, 1).Render("[↻ Refresh]")
7728+
liveIndicator := lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF7F")).Render("● live")
7729+
content.WriteString(refreshBtn + " " + dimSt.Render("Enter → reload") + " " + liveIndicator + dimSt.Render(" (auto every 10s)") + "\n\n")
7730+
7731+
var logsContent strings.Builder
7732+
if !m.dbLogsLoaded {
7733+
logsContent.WriteString(dimSt.Render("Loading..."))
7734+
} else if len(m.dbDetailLogs) == 0 {
7735+
logsContent.WriteString(dimSt.Render("No log entries found"))
7736+
} else {
7737+
timeSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
7738+
hostSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#7B68EE"))
7739+
msgSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF"))
7740+
indentSt := lipgloss.NewStyle().Foreground(lipgloss.Color("#666666"))
7741+
// Derive maxVisible from terminal height.
7742+
// Each entry uses 2 lines (header + message), so halve available lines.
7743+
maxVisible := (m.height - 27) / 2
7744+
if maxVisible < 3 {
7745+
maxVisible = 3
7746+
}
7747+
// Show entries most-recent first, with scroll window
7748+
total := len(m.dbDetailLogs)
7749+
startIdx := m.dbLogsScrollOffset
7750+
if startIdx > total-1 {
7751+
startIdx = total - 1
7752+
}
7753+
if startIdx < 0 {
7754+
startIdx = 0
7755+
}
7756+
endIdx := startIdx + maxVisible
7757+
if endIdx > total {
7758+
endIdx = total
7759+
}
7760+
scrollHint := lipgloss.NewStyle().Foreground(lipgloss.Color("#444444")).Render(
7761+
fmt.Sprintf(" ↑/↓ scroll (%d-%d / %d)", startIdx+1, endIdx, total))
7762+
logsContent.WriteString(scrollHint + "\n")
7763+
// iterate reversed (newest = index 0)
7764+
for ri := startIdx; ri < endIdx; ri++ {
7765+
i := total - 1 - ri // actual index in m.dbDetailLogs
7766+
e := m.dbDetailLogs[i]
7767+
msgStr := getStringValue(e, "message", "")
7768+
host := getStringValue(e, "hostname", "")
7769+
ts := ""
7770+
if v, ok := toFloat64(e["timestamp"]); ok {
7771+
t2 := time.Unix(int64(v), 0).UTC()
7772+
ts = t2.Format("2006-01-02 15:04:05")
7773+
}
7774+
// Line 1: timestamp hostname
7775+
logsContent.WriteString(timeSt.Render(ts) + " " + hostSt.Render(host) + "\n")
7776+
// Line 2: indented full message
7777+
logsContent.WriteString(indentSt.Render(" ↳ ") + msgSt.Render(msgStr) + "\n")
7778+
}
7779+
}
7780+
content.WriteString(renderBox(
7781+
fmt.Sprintf("Logs (%d)", len(m.dbDetailLogs)),
7782+
strings.TrimRight(logsContent.String(), "\n"),
7783+
fullWidth))
76937784
}
76947785

76957786
return content.String()
@@ -9533,6 +9624,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
95339624
m.dbUserSelectedIdx = -1
95349625
m.dbUserDeleteConfirm = false
95359626
m.dbPoolCreateStep = -1
9627+
m.dbLogsScrollOffset = 0
95369628
}
95379629
return m, nil
95389630
}
@@ -9694,13 +9786,20 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
96949786
}
96959787
// In DetailView for ManagedDatabases/Analytics, → switches tabs
96969788
if m.mode == DetailView && (m.currentProduct == ProductManagedDatabases || m.currentProduct == ProductManagedAnalytics) {
9697-
maxTab := 4
9789+
maxTab := 5
96989790
if m.dbDetailTab < maxTab {
96999791
m.dbDetailTab++
97009792
m.actionConfirm = false
97019793
m.dbUserSelectedIdx = -1
97029794
m.dbUserDeleteConfirm = false
97039795
m.dbPoolCreateStep = -1
9796+
m.dbLogsScrollOffset = 0
9797+
// Entering Logs tab: trigger fetch
9798+
if m.dbDetailTab == 5 {
9799+
m.dbLogsLoaded = false
9800+
m.dbDetailLogs = nil
9801+
return m, m.fetchDBLogs()
9802+
}
97049803
}
97059804
return m, nil
97069805
}
@@ -10580,6 +10679,11 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
1058010679
m.dbPoolCreateMode = 0
1058110680
m.dbPoolCreateSize = "10"
1058210681
}
10682+
case 5: // Logs tab — refresh
10683+
m.dbLogsLoaded = false
10684+
m.dbDetailLogs = nil
10685+
m.dbLogsScrollOffset = 0
10686+
return m, m.fetchDBLogs()
1058310687
}
1058410688
return m, nil
1058510689
} else if m.mode == DetailView && m.currentProduct == ProductInstances {
@@ -10976,6 +11080,9 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
1097611080
m.dbDBCreateMode = false
1097711081
m.dbDBCreateInput = ""
1097811082
m.dbPoolCreateStep = -1
11083+
m.dbDetailLogs = nil
11084+
m.dbLogsLoaded = false
11085+
m.dbLogsScrollOffset = 0
1097911086
if engine != "" && serviceId != "" {
1098011087
return m, m.fetchDBDetailSubresources(engine, serviceId)
1098111088
}
@@ -10992,6 +11099,27 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
1099211099
isSubNavProduct := isStorageSubProduct || isNetworkSubProduct || isComputeSubProduct
1099311100
navItems := getNavItems()
1099411101

11102+
// DB Logs tab: ↑/↓ scroll
11103+
if m.mode == DetailView && (m.currentProduct == ProductManagedDatabases || m.currentProduct == ProductManagedAnalytics) &&
11104+
m.dbDetailTab == 5 && m.dbLogsLoaded {
11105+
total := len(m.dbDetailLogs)
11106+
maxVisible := 20
11107+
maxOffset := total - maxVisible
11108+
if maxOffset < 0 {
11109+
maxOffset = 0
11110+
}
11111+
if key == "up" || key == "k" {
11112+
if m.dbLogsScrollOffset < maxOffset {
11113+
m.dbLogsScrollOffset++
11114+
}
11115+
} else if key == "down" || key == "j" {
11116+
if m.dbLogsScrollOffset > 0 {
11117+
m.dbLogsScrollOffset--
11118+
}
11119+
}
11120+
return m, nil
11121+
}
11122+
1099511123
// DB Users tab: ↑/↓ navigate user rows
1099611124
if m.mode == DetailView && (m.currentProduct == ProductManagedDatabases || m.currentProduct == ProductManagedAnalytics) &&
1099711125
m.dbDetailTab == 1 && !m.dbUserCreateMode && m.dbUserCreatedData == nil {

0 commit comments

Comments
 (0)