avoid vcenter totals pages scanning whole database
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
@@ -56,7 +56,7 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- cp /shared/vctp-linux-amd64 ./build/vctp-linux-amd64
|
- cp /shared/vctp-linux-amd64 ./build/vctp-linux-amd64
|
||||||
#- find .
|
#- find .
|
||||||
- nfpm package --config vctp.yml --packager rpm --target ./build/
|
- nfpm package --config vctp-service.yml --packager rpm --target ./build/
|
||||||
- ls -lah ./build/
|
- ls -lah ./build/
|
||||||
|
|
||||||
- name: dell-sftp-deploy
|
- name: dell-sftp-deploy
|
||||||
|
|||||||
@@ -80,6 +80,13 @@ use:
|
|||||||
vctp -settings /path/to/vctp.yml -db-cleanup
|
vctp -settings /path/to/vctp.yml -db-cleanup
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want a one-time cache backfill for the vCenter totals cache tables
|
||||||
|
(`vcenter_latest_totals` and `vcenter_aggregate_totals`) and exit, use:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
vctp -settings /path/to/vctp.yml -backfill-vcenter-cache
|
||||||
|
```
|
||||||
|
|
||||||
## Database Configuration
|
## Database Configuration
|
||||||
By default the app uses SQLite and creates/opens `db.sqlite3`.
|
By default the app uses SQLite and creates/opens `db.sqlite3`.
|
||||||
|
|
||||||
|
|||||||
@@ -27,10 +27,8 @@ type VcenterTotalsMeta struct {
|
|||||||
TypeLabel string
|
TypeLabel string
|
||||||
HourlyLink string
|
HourlyLink string
|
||||||
DailyLink string
|
DailyLink string
|
||||||
MonthlyLink string
|
|
||||||
HourlyClass string
|
HourlyClass string
|
||||||
DailyClass string
|
DailyClass string
|
||||||
MonthlyClass string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type VcenterChartData struct {
|
type VcenterChartData struct {
|
||||||
@@ -177,9 +175,8 @@ templ VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcen
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="web3-button-group mt-8 mb-3">
|
<div class="web3-button-group mt-8 mb-3">
|
||||||
<a class={ meta.HourlyClass } href={ meta.HourlyLink }>Hourly</a>
|
<a class={ meta.HourlyClass } href={ meta.HourlyLink }>Hourly Detail (45d)</a>
|
||||||
<a class={ meta.DailyClass } href={ meta.DailyLink }>Daily</a>
|
<a class={ meta.DailyClass } href={ meta.DailyLink }>Daily Aggregated</a>
|
||||||
<a class={ meta.MonthlyClass } href={ meta.MonthlyLink }>Monthly</a>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="web2-card">
|
<section class="web2-card">
|
||||||
|
|||||||
@@ -35,10 +35,8 @@ type VcenterTotalsMeta struct {
|
|||||||
TypeLabel string
|
TypeLabel string
|
||||||
HourlyLink string
|
HourlyLink string
|
||||||
DailyLink string
|
DailyLink string
|
||||||
MonthlyLink string
|
|
||||||
HourlyClass string
|
HourlyClass string
|
||||||
DailyClass string
|
DailyClass string
|
||||||
MonthlyClass string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type VcenterChartData struct {
|
type VcenterChartData struct {
|
||||||
@@ -168,7 +166,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var5 string
|
var templ_7745c5c3_Var5 string
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 62, Col: 50}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 60, Col: 50}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -181,7 +179,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle)
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 63, Col: 56}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 61, Col: 56}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -194,7 +192,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 71, Col: 45}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 69, Col: 45}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -213,7 +211,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group)
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 86, Col: 77}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 84, Col: 77}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -231,7 +229,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var9 string
|
var templ_7745c5c3_Var9 string
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label)
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 92, Col: 76}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 90, Col: 76}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -244,7 +242,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var10 string
|
var templ_7745c5c3_Var10 string
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Count)
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Count)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 96, Col: 49}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 94, Col: 49}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -257,7 +255,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var11 templ.SafeURL
|
var templ_7745c5c3_Var11 templ.SafeURL
|
||||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 99, Col: 49}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 97, Col: 49}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -320,7 +318,7 @@ func VcenterList(links []VcenterLink) templ.Component {
|
|||||||
var templ_7745c5c3_Var13 string
|
var templ_7745c5c3_Var13 string
|
||||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(len(links))
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(len(links))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 132, Col: 43}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 130, Col: 43}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -338,7 +336,7 @@ func VcenterList(links []VcenterLink) templ.Component {
|
|||||||
var templ_7745c5c3_Var14 string
|
var templ_7745c5c3_Var14 string
|
||||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name)
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 145, Col: 62}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 143, Col: 62}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -351,7 +349,7 @@ func VcenterList(links []VcenterLink) templ.Component {
|
|||||||
var templ_7745c5c3_Var15 templ.SafeURL
|
var templ_7745c5c3_Var15 templ.SafeURL
|
||||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(link.Link)
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(link.Link)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 147, Col: 48}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 145, Col: 48}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -414,7 +412,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent
|
|||||||
var templ_7745c5c3_Var17 string
|
var templ_7745c5c3_Var17 string
|
||||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vcenter)
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vcenter)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 171, Col: 63}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 169, Col: 63}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -427,7 +425,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent
|
|||||||
var templ_7745c5c3_Var18 string
|
var templ_7745c5c3_Var18 string
|
||||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel)
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 172, Col: 62}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 170, Col: 62}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -462,13 +460,13 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent
|
|||||||
var templ_7745c5c3_Var21 templ.SafeURL
|
var templ_7745c5c3_Var21 templ.SafeURL
|
||||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(meta.HourlyLink)
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(meta.HourlyLink)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 180, Col: 58}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 178, Col: 59}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">Hourly</a> ")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">Hourly Detail (45d)</a> ")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -497,159 +495,124 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent
|
|||||||
var templ_7745c5c3_Var24 templ.SafeURL
|
var templ_7745c5c3_Var24 templ.SafeURL
|
||||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(meta.DailyLink)
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(meta.DailyLink)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 181, Col: 56}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 179, Col: 57}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">Daily</a> ")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">Daily Aggregated</a></div></section><section class=\"web2-card\"><div class=\"flex items-center justify-between gap-3 mb-4 flex-wrap\"><h2 class=\"text-lg font-semibold\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var25 = []any{meta.MonthlyClass}
|
var templ_7745c5c3_Var25 string
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var25...)
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 184, Col: 56}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<a class=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " Snapshots</h2><span class=\"web2-badge\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var26 string
|
var templ_7745c5c3_Var26 string
|
||||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var25).String())
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 1, Col: 0}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 185, Col: 45}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" href=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " records</span></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var27 templ.SafeURL
|
if chart.ConfigJSON != "" {
|
||||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(meta.MonthlyLink)
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div class=\"mb-6 overflow-auto\"><div class=\"web3-chart-frame\"><canvas id=\"vcenter-totals-chart\" class=\"web3-chart-canvas\" role=\"img\" aria-label=\"Totals over time\" data-chart-config=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 182, Col: 60}
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var27 string
|
||||||
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(chart.ConfigJSON)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 190, Col: 145}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">Monthly</a></div></section><section class=\"web2-card\"><div class=\"flex items-center justify-between gap-3 mb-4 flex-wrap\"><h2 class=\"text-lg font-semibold\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"></canvas><div id=\"vcenter-totals-tooltip\" class=\"web3-chart-tooltip\" aria-hidden=\"true\"></div></div><script>\n\t\t\t\t\t\t\t\twindow.Web3Charts.renderFromDataset({\n\t\t\t\t\t\t\t\t\tcanvasId: \"vcenter-totals-chart\",\n\t\t\t\t\t\t\t\t\ttooltipId: \"vcenter-totals-tooltip\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t</script></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div class=\"overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><thead><tr><th>Snapshot Time</th><th class=\"text-right\">VMs</th><th class=\"text-right\">vCPUs</th><th class=\"text-right\">RAM (GB)</th></tr></thead> <tbody>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<tr><td>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var28 string
|
var templ_7745c5c3_Var28 string
|
||||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel)
|
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Snapshot)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 187, Col: 56}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 214, Col: 30}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " Snapshots</h2><span class=\"web2-badge\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</td><td class=\"text-right\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var29 string
|
var templ_7745c5c3_Var29 string
|
||||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 188, Col: 45}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 215, Col: 48}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " records</span></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</td><td class=\"text-right\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if chart.ConfigJSON != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"mb-6 overflow-auto\"><div class=\"web3-chart-frame\"><canvas id=\"vcenter-totals-chart\" class=\"web3-chart-canvas\" role=\"img\" aria-label=\"Totals over time\" data-chart-config=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var30 string
|
var templ_7745c5c3_Var30 string
|
||||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(chart.ConfigJSON)
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 193, Col: 145}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 216, Col: 50}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\"></canvas><div id=\"vcenter-totals-tooltip\" class=\"web3-chart-tooltip\" aria-hidden=\"true\"></div></div><script>\n\t\t\t\t\t\t\t\twindow.Web3Charts.renderFromDataset({\n\t\t\t\t\t\t\t\t\tcanvasId: \"vcenter-totals-chart\",\n\t\t\t\t\t\t\t\t\ttooltipId: \"vcenter-totals-tooltip\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t</script></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</td><td class=\"text-right\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><thead><tr><th>Snapshot Time</th><th class=\"text-right\">VMs</th><th class=\"text-right\">vCPUs</th><th class=\"text-right\">RAM (GB)</th></tr></thead> <tbody>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
for _, entry := range entries {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<tr><td>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var31 string
|
var templ_7745c5c3_Var31 string
|
||||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Snapshot)
|
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 30}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 51}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</td><td class=\"text-right\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</td></tr>")
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var32 string
|
|
||||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 218, Col: 48}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</td><td class=\"text-right\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var33 string
|
|
||||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 219, Col: 50}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</td><td class=\"text-right\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var34 string
|
|
||||||
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 220, Col: 51}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</td></tr>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</tbody></table></div></section></main></body>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</tbody></table></div></section></main></body>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -657,7 +620,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</html>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</html>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
384
db/helpers.go
384
db/helpers.go
@@ -1121,6 +1121,277 @@ CREATE TABLE IF NOT EXISTS vcenter_totals (
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureVcenterLatestTotalsTable creates a compact table with one latest totals row per vCenter.
|
||||||
|
func EnsureVcenterLatestTotalsTable(ctx context.Context, dbConn *sqlx.DB) error {
|
||||||
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
|
var ddl string
|
||||||
|
switch driver {
|
||||||
|
case "pgx", "postgres":
|
||||||
|
ddl = `
|
||||||
|
CREATE TABLE IF NOT EXISTS vcenter_latest_totals (
|
||||||
|
"Vcenter" TEXT PRIMARY KEY,
|
||||||
|
"SnapshotTime" BIGINT NOT NULL,
|
||||||
|
"VmCount" BIGINT NOT NULL,
|
||||||
|
"VcpuTotal" BIGINT NOT NULL,
|
||||||
|
"RamTotalGB" BIGINT NOT NULL
|
||||||
|
);`
|
||||||
|
default:
|
||||||
|
ddl = `
|
||||||
|
CREATE TABLE IF NOT EXISTS vcenter_latest_totals (
|
||||||
|
"Vcenter" TEXT PRIMARY KEY,
|
||||||
|
"SnapshotTime" BIGINT NOT NULL,
|
||||||
|
"VmCount" BIGINT NOT NULL,
|
||||||
|
"VcpuTotal" BIGINT NOT NULL,
|
||||||
|
"RamTotalGB" BIGINT NOT NULL
|
||||||
|
);`
|
||||||
|
}
|
||||||
|
if _, err := execLog(ctx, dbConn, ddl); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertVcenterLatestTotals stores the latest totals per vCenter.
|
||||||
|
func UpsertVcenterLatestTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotTime int64, vmCount, vcpuTotal, ramTotal int64) error {
|
||||||
|
if strings.TrimSpace(vcenter) == "" {
|
||||||
|
return fmt.Errorf("vcenter is empty")
|
||||||
|
}
|
||||||
|
if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := execLog(ctx, dbConn, `
|
||||||
|
INSERT INTO vcenter_latest_totals ("Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB")
|
||||||
|
VALUES ($1,$2,$3,$4,$5)
|
||||||
|
ON CONFLICT ("Vcenter") DO UPDATE SET
|
||||||
|
"SnapshotTime" = EXCLUDED."SnapshotTime",
|
||||||
|
"VmCount" = EXCLUDED."VmCount",
|
||||||
|
"VcpuTotal" = EXCLUDED."VcpuTotal",
|
||||||
|
"RamTotalGB" = EXCLUDED."RamTotalGB"
|
||||||
|
WHERE EXCLUDED."SnapshotTime" >= vcenter_latest_totals."SnapshotTime"
|
||||||
|
`, vcenter, snapshotTime, vmCount, vcpuTotal, ramTotal)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureVcenterAggregateTotalsTable creates a compact cache for hourly/daily (and optional monthly) totals.
|
||||||
|
func EnsureVcenterAggregateTotalsTable(ctx context.Context, dbConn *sqlx.DB) error {
|
||||||
|
ddl := `
|
||||||
|
CREATE TABLE IF NOT EXISTS vcenter_aggregate_totals (
|
||||||
|
"SnapshotType" TEXT NOT NULL,
|
||||||
|
"Vcenter" TEXT NOT NULL,
|
||||||
|
"SnapshotTime" BIGINT NOT NULL,
|
||||||
|
"VmCount" BIGINT NOT NULL,
|
||||||
|
"VcpuTotal" BIGINT NOT NULL,
|
||||||
|
"RamTotalGB" BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY ("SnapshotType","Vcenter","SnapshotTime")
|
||||||
|
);`
|
||||||
|
if _, err := execLog(ctx, dbConn, ddl); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, _ = execLog(ctx, dbConn, `CREATE INDEX IF NOT EXISTS vcenter_aggregate_totals_vc_type_time_idx ON vcenter_aggregate_totals ("Vcenter","SnapshotType","SnapshotTime" DESC)`)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertVcenterAggregateTotal stores per-vCenter totals for a snapshot type/time.
|
||||||
|
func UpsertVcenterAggregateTotal(ctx context.Context, dbConn *sqlx.DB, snapshotType, vcenter string, snapshotTime int64, vmCount, vcpuTotal, ramTotal int64) error {
|
||||||
|
snapshotType = strings.ToLower(strings.TrimSpace(snapshotType))
|
||||||
|
if snapshotType == "" {
|
||||||
|
return fmt.Errorf("snapshot type is empty")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(vcenter) == "" {
|
||||||
|
return fmt.Errorf("vcenter is empty")
|
||||||
|
}
|
||||||
|
if err := EnsureVcenterAggregateTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := execLog(ctx, dbConn, `
|
||||||
|
INSERT INTO vcenter_aggregate_totals ("SnapshotType","Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB")
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6)
|
||||||
|
ON CONFLICT ("SnapshotType","Vcenter","SnapshotTime") DO UPDATE SET
|
||||||
|
"VmCount" = EXCLUDED."VmCount",
|
||||||
|
"VcpuTotal" = EXCLUDED."VcpuTotal",
|
||||||
|
"RamTotalGB" = EXCLUDED."RamTotalGB"
|
||||||
|
`, snapshotType, vcenter, snapshotTime, vmCount, vcpuTotal, ramTotal)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceVcenterAggregateTotalsFromSummary recomputes one snapshot's per-vCenter totals from a summary table.
|
||||||
|
func ReplaceVcenterAggregateTotalsFromSummary(ctx context.Context, dbConn *sqlx.DB, summaryTable, snapshotType string, snapshotTime int64) (int, error) {
|
||||||
|
if err := ValidateTableName(summaryTable); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
snapshotType = strings.ToLower(strings.TrimSpace(snapshotType))
|
||||||
|
if snapshotType == "" {
|
||||||
|
return 0, fmt.Errorf("snapshot type is empty")
|
||||||
|
}
|
||||||
|
if err := EnsureVcenterAggregateTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if _, err := execLog(ctx, dbConn, `
|
||||||
|
DELETE FROM vcenter_aggregate_totals
|
||||||
|
WHERE "SnapshotType" = $1 AND "SnapshotTime" = $2
|
||||||
|
`, snapshotType, snapshotTime); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
"Vcenter" AS vcenter,
|
||||||
|
COUNT(1) AS vm_count,
|
||||||
|
CAST(COALESCE(SUM(COALESCE("AvgVcpuCount","VcpuCount")),0) AS BIGINT) AS vcpu_total,
|
||||||
|
CAST(COALESCE(SUM(COALESCE("AvgRamGB","RamGB")),0) AS BIGINT) AS ram_total
|
||||||
|
FROM %s
|
||||||
|
GROUP BY "Vcenter"
|
||||||
|
`, summaryTable)
|
||||||
|
var rows []struct {
|
||||||
|
Vcenter string `db:"vcenter"`
|
||||||
|
VmCount int64 `db:"vm_count"`
|
||||||
|
VcpuTotal int64 `db:"vcpu_total"`
|
||||||
|
RamTotal int64 `db:"ram_total"`
|
||||||
|
}
|
||||||
|
if err := selectLog(ctx, dbConn, &rows, query); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
upserted := 0
|
||||||
|
for _, row := range rows {
|
||||||
|
if err := UpsertVcenterAggregateTotal(ctx, dbConn, snapshotType, row.Vcenter, snapshotTime, row.VmCount, row.VcpuTotal, row.RamTotal); err != nil {
|
||||||
|
return upserted, err
|
||||||
|
}
|
||||||
|
upserted++
|
||||||
|
}
|
||||||
|
return upserted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncVcenterAggregateTotalsFromRegistry refreshes cached totals for summary snapshots listed in snapshot_registry.
|
||||||
|
func SyncVcenterAggregateTotalsFromRegistry(ctx context.Context, dbConn *sqlx.DB, snapshotType string) (int, int, error) {
|
||||||
|
snapshotType = strings.ToLower(strings.TrimSpace(snapshotType))
|
||||||
|
if snapshotType == "" {
|
||||||
|
return 0, 0, fmt.Errorf("snapshot type is empty")
|
||||||
|
}
|
||||||
|
if snapshotType != "daily" && snapshotType != "monthly" {
|
||||||
|
return 0, 0, fmt.Errorf("unsupported snapshot type for summary cache sync: %s", snapshotType)
|
||||||
|
}
|
||||||
|
if err := EnsureVcenterAggregateTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := dbConn.Rebind(`
|
||||||
|
SELECT table_name, snapshot_time
|
||||||
|
FROM snapshot_registry
|
||||||
|
WHERE snapshot_type = ?
|
||||||
|
ORDER BY snapshot_time
|
||||||
|
`)
|
||||||
|
var snapshots []struct {
|
||||||
|
TableName string `db:"table_name"`
|
||||||
|
SnapshotTime int64 `db:"snapshot_time"`
|
||||||
|
}
|
||||||
|
if err := selectLog(ctx, dbConn, &snapshots, query, snapshotType); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotsRefreshed := 0
|
||||||
|
rowsUpserted := 0
|
||||||
|
failures := 0
|
||||||
|
for _, snapshot := range snapshots {
|
||||||
|
if err := ValidateTableName(snapshot.TableName); err != nil {
|
||||||
|
failures++
|
||||||
|
slog.Warn("skipping invalid summary table in snapshot registry", "snapshot_type", snapshotType, "table", snapshot.TableName, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
upserted, err := ReplaceVcenterAggregateTotalsFromSummary(ctx, dbConn, snapshot.TableName, snapshotType, snapshot.SnapshotTime)
|
||||||
|
if err != nil {
|
||||||
|
failures++
|
||||||
|
slog.Warn("failed to refresh vcenter aggregate cache from summary table", "snapshot_type", snapshotType, "table", snapshot.TableName, "snapshot_time", snapshot.SnapshotTime, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
snapshotsRefreshed++
|
||||||
|
rowsUpserted += upserted
|
||||||
|
}
|
||||||
|
|
||||||
|
if failures > 0 {
|
||||||
|
return snapshotsRefreshed, rowsUpserted, fmt.Errorf("vcenter aggregate cache sync finished with %d failed snapshot(s)", failures)
|
||||||
|
}
|
||||||
|
return snapshotsRefreshed, rowsUpserted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVcenterAggregateTotals lists cached totals by type.
|
||||||
|
func ListVcenterAggregateTotals(ctx context.Context, dbConn *sqlx.DB, vcenter, snapshotType string, limit int) ([]VcenterTotalRow, error) {
|
||||||
|
snapshotType = strings.ToLower(strings.TrimSpace(snapshotType))
|
||||||
|
if err := EnsureVcenterAggregateTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
rows := make([]VcenterTotalRow, 0, limit)
|
||||||
|
query := `
|
||||||
|
SELECT "Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB"
|
||||||
|
FROM vcenter_aggregate_totals
|
||||||
|
WHERE "Vcenter" = $1 AND "SnapshotType" = $2
|
||||||
|
ORDER BY "SnapshotTime" DESC
|
||||||
|
LIMIT $3
|
||||||
|
`
|
||||||
|
if err := selectLog(ctx, dbConn, &rows, query, vcenter, snapshotType, limit); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListVcenterAggregateTotalsSince lists cached totals by type from a lower-bound timestamp.
|
||||||
|
func ListVcenterAggregateTotalsSince(ctx context.Context, dbConn *sqlx.DB, vcenter, snapshotType string, since time.Time) ([]VcenterTotalRow, error) {
|
||||||
|
snapshotType = strings.ToLower(strings.TrimSpace(snapshotType))
|
||||||
|
if err := EnsureVcenterAggregateTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows := make([]VcenterTotalRow, 0, 256)
|
||||||
|
query := `
|
||||||
|
SELECT "Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB"
|
||||||
|
FROM vcenter_aggregate_totals
|
||||||
|
WHERE "Vcenter" = $1 AND "SnapshotType" = $2 AND "SnapshotTime" >= $3
|
||||||
|
ORDER BY "SnapshotTime" DESC
|
||||||
|
`
|
||||||
|
if err := selectLog(ctx, dbConn, &rows, query, vcenter, snapshotType, since.Unix()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncVcenterLatestTotalsFromHistory backfills latest totals from existing vcenter_totals history.
|
||||||
|
func SyncVcenterLatestTotalsFromHistory(ctx context.Context, dbConn *sqlx.DB) (int, error) {
|
||||||
|
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var rows []struct {
|
||||||
|
Vcenter string `db:"Vcenter"`
|
||||||
|
SnapshotTime int64 `db:"SnapshotTime"`
|
||||||
|
VmCount int64 `db:"VmCount"`
|
||||||
|
VcpuTotal int64 `db:"VcpuTotal"`
|
||||||
|
RamTotalGB int64 `db:"RamTotalGB"`
|
||||||
|
}
|
||||||
|
if err := selectLog(ctx, dbConn, &rows, `
|
||||||
|
SELECT t."Vcenter", t."SnapshotTime", t."VmCount", t."VcpuTotal", t."RamTotalGB"
|
||||||
|
FROM vcenter_totals t
|
||||||
|
JOIN (
|
||||||
|
SELECT "Vcenter", MAX("SnapshotTime") AS max_snapshot_time
|
||||||
|
FROM vcenter_totals
|
||||||
|
GROUP BY "Vcenter"
|
||||||
|
) latest
|
||||||
|
ON latest."Vcenter" = t."Vcenter"
|
||||||
|
AND latest.max_snapshot_time = t."SnapshotTime"
|
||||||
|
`); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
upserted := 0
|
||||||
|
for _, row := range rows {
|
||||||
|
if err := UpsertVcenterLatestTotals(ctx, dbConn, row.Vcenter, row.SnapshotTime, row.VmCount, row.VcpuTotal, row.RamTotalGB); err != nil {
|
||||||
|
return upserted, err
|
||||||
|
}
|
||||||
|
upserted++
|
||||||
|
}
|
||||||
|
return upserted, nil
|
||||||
|
}
|
||||||
|
|
||||||
// InsertVcenterTotals records totals for a vcenter at a snapshot time.
|
// InsertVcenterTotals records totals for a vcenter at a snapshot time.
|
||||||
func InsertVcenterTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotTime time.Time, vmCount, vcpuTotal, ramTotal int64) error {
|
func InsertVcenterTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotTime time.Time, vmCount, vcpuTotal, ramTotal int64) error {
|
||||||
if strings.TrimSpace(vcenter) == "" {
|
if strings.TrimSpace(vcenter) == "" {
|
||||||
@@ -1129,15 +1400,56 @@ func InsertVcenterTotals(ctx context.Context, dbConn *sqlx.DB, vcenter string, s
|
|||||||
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
_, err := execLog(ctx, dbConn, `
|
_, err := execLog(ctx, dbConn, `
|
||||||
INSERT INTO vcenter_totals ("Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB")
|
INSERT INTO vcenter_totals ("Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB")
|
||||||
VALUES ($1,$2,$3,$4,$5)
|
VALUES ($1,$2,$3,$4,$5)
|
||||||
`, vcenter, snapshotTime.Unix(), vmCount, vcpuTotal, ramTotal)
|
`, vcenter, snapshotTime.Unix(), vmCount, vcpuTotal, ramTotal)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := UpsertVcenterLatestTotals(ctx, dbConn, vcenter, snapshotTime.Unix(), vmCount, vcpuTotal, ramTotal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := UpsertVcenterAggregateTotal(ctx, dbConn, "hourly", vcenter, snapshotTime.Unix(), vmCount, vcpuTotal, ramTotal); err != nil {
|
||||||
|
slog.Warn("failed to upsert vcenter_aggregate_totals", "snapshot_type", "hourly", "vcenter", vcenter, "snapshot_time", snapshotTime.Unix(), "error", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListVcenters returns distinct vcenter URLs tracked.
|
// ListVcenters returns distinct vcenter URLs tracked.
|
||||||
func ListVcenters(ctx context.Context, dbConn *sqlx.DB) ([]string, error) {
|
func ListVcenters(ctx context.Context, dbConn *sqlx.DB) ([]string, error) {
|
||||||
|
if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err == nil {
|
||||||
|
rows, err := dbConn.QueryxContext(ctx, `SELECT "Vcenter" FROM vcenter_latest_totals ORDER BY "Vcenter"`)
|
||||||
|
if err == nil {
|
||||||
|
defer rows.Close()
|
||||||
|
out := make([]string, 0, 32)
|
||||||
|
for rows.Next() {
|
||||||
|
var v string
|
||||||
|
if err := rows.Scan(&v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(out) > 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
// Older installs may have vcenter_totals populated but no latest cache yet.
|
||||||
|
if _, err := SyncVcenterLatestTotalsFromHistory(ctx, dbConn); err == nil {
|
||||||
|
refreshed := make([]string, 0, 32)
|
||||||
|
if err := selectLog(ctx, dbConn, &refreshed, `SELECT "Vcenter" FROM vcenter_latest_totals ORDER BY "Vcenter"`); err == nil && len(refreshed) > 0 {
|
||||||
|
return refreshed, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for older DBs before vcenter_latest_totals gets populated.
|
||||||
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1187,15 +1499,66 @@ LIMIT $2`
|
|||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListVcenterHourlyTotalsSince returns hourly totals for a vCenter from a minimum snapshot time.
|
||||||
|
func ListVcenterHourlyTotalsSince(ctx context.Context, dbConn *sqlx.DB, vcenter string, since time.Time) ([]VcenterTotalRow, error) {
|
||||||
|
cachedRows, cacheErr := ListVcenterAggregateTotalsSince(ctx, dbConn, vcenter, "hourly", since)
|
||||||
|
if cacheErr == nil && len(cachedRows) > 0 {
|
||||||
|
return cachedRows, nil
|
||||||
|
}
|
||||||
|
if cacheErr != nil {
|
||||||
|
slog.Warn("failed to read hourly totals cache", "vcenter", vcenter, "since", since.Unix(), "error", cacheErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows := make([]VcenterTotalRow, 0, 256)
|
||||||
|
query := `
|
||||||
|
SELECT "Vcenter","SnapshotTime","VmCount","VcpuTotal","RamTotalGB"
|
||||||
|
FROM vcenter_totals
|
||||||
|
WHERE "Vcenter" = $1
|
||||||
|
AND "SnapshotTime" >= $2
|
||||||
|
ORDER BY "SnapshotTime" DESC`
|
||||||
|
if err := selectLog(ctx, dbConn, &rows, query, vcenter, since.Unix()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
if err := UpsertVcenterAggregateTotal(ctx, dbConn, "hourly", row.Vcenter, row.SnapshotTime, row.VmCount, row.VcpuTotal, row.RamTotalGB); err != nil {
|
||||||
|
slog.Warn("failed to warm hourly totals cache", "vcenter", row.Vcenter, "snapshot_time", row.SnapshotTime, "error", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListVcenterTotalsByType returns totals for a vcenter for the requested snapshot type (hourly, daily, monthly).
|
// ListVcenterTotalsByType returns totals for a vcenter for the requested snapshot type (hourly, daily, monthly).
|
||||||
// Hourly values come from vcenter_totals; daily/monthly are derived from the summary tables referenced in snapshot_registry.
|
// Prefer vcenter_aggregate_totals cache and fallback to source tables when cache is empty.
|
||||||
func ListVcenterTotalsByType(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotType string, limit int) ([]VcenterTotalRow, error) {
|
func ListVcenterTotalsByType(ctx context.Context, dbConn *sqlx.DB, vcenter string, snapshotType string, limit int) ([]VcenterTotalRow, error) {
|
||||||
snapshotType = strings.ToLower(snapshotType)
|
snapshotType = strings.ToLower(strings.TrimSpace(snapshotType))
|
||||||
if snapshotType == "" {
|
if snapshotType == "" {
|
||||||
snapshotType = "hourly"
|
snapshotType = "hourly"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cachedRows, cacheErr := ListVcenterAggregateTotals(ctx, dbConn, vcenter, snapshotType, limit)
|
||||||
|
if cacheErr == nil && len(cachedRows) > 0 {
|
||||||
|
return cachedRows, nil
|
||||||
|
}
|
||||||
|
if cacheErr != nil {
|
||||||
|
slog.Warn("failed to read vcenter aggregate totals cache", "snapshot_type", snapshotType, "vcenter", vcenter, "error", cacheErr)
|
||||||
|
}
|
||||||
|
|
||||||
if snapshotType == "hourly" {
|
if snapshotType == "hourly" {
|
||||||
return ListVcenterTotals(ctx, dbConn, vcenter, limit)
|
rows, err := ListVcenterTotals(ctx, dbConn, vcenter, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
if err := UpsertVcenterAggregateTotal(ctx, dbConn, "hourly", row.Vcenter, row.SnapshotTime, row.VmCount, row.VcpuTotal, row.RamTotalGB); err != nil {
|
||||||
|
slog.Warn("failed to warm hourly totals cache", "vcenter", row.Vcenter, "snapshot_time", row.SnapshotTime, "error", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
@@ -1240,6 +1603,12 @@ LIMIT $2
|
|||||||
RamTotalGB: agg.RamTotalGB,
|
RamTotalGB: agg.RamTotalGB,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
for _, row := range out {
|
||||||
|
if err := UpsertVcenterAggregateTotal(ctx, dbConn, snapshotType, row.Vcenter, row.SnapshotTime, row.VmCount, row.VcpuTotal, row.RamTotalGB); err != nil {
|
||||||
|
slog.Warn("failed to warm vcenter aggregate totals cache", "snapshot_type", snapshotType, "vcenter", row.Vcenter, "snapshot_time", row.SnapshotTime, "error", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1589,6 +1958,9 @@ func SyncVcenterTotalsFromSnapshots(ctx context.Context, dbConn *sqlx.DB) error
|
|||||||
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
if err := EnsureVcenterTotalsTable(ctx, dbConn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
driver := strings.ToLower(dbConn.DriverName())
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
var hourlyTables []struct {
|
var hourlyTables []struct {
|
||||||
TableName string `db:"table_name"`
|
TableName string `db:"table_name"`
|
||||||
@@ -1640,6 +2012,12 @@ WHERE NOT EXISTS (
|
|||||||
if _, err := execLog(ctx, dbConn, insert, a.Vcenter, ht.SnapshotTime, a.VmCount, a.VcpuTotal, a.RamTotal); err != nil {
|
if _, err := execLog(ctx, dbConn, insert, a.Vcenter, ht.SnapshotTime, a.VmCount, a.VcpuTotal, a.RamTotal); err != nil {
|
||||||
slog.Warn("failed to backfill vcenter_totals", "table", ht.TableName, "vcenter", a.Vcenter, "snapshot_time", ht.SnapshotTime, "error", err)
|
slog.Warn("failed to backfill vcenter_totals", "table", ht.TableName, "vcenter", a.Vcenter, "snapshot_time", ht.SnapshotTime, "error", err)
|
||||||
}
|
}
|
||||||
|
if err := UpsertVcenterLatestTotals(ctx, dbConn, a.Vcenter, ht.SnapshotTime, a.VmCount, a.VcpuTotal, a.RamTotal); err != nil {
|
||||||
|
slog.Warn("failed to upsert vcenter_latest_totals", "table", ht.TableName, "vcenter", a.Vcenter, "snapshot_time", ht.SnapshotTime, "error", err)
|
||||||
|
}
|
||||||
|
if err := UpsertVcenterAggregateTotal(ctx, dbConn, "hourly", a.Vcenter, ht.SnapshotTime, a.VmCount, a.VcpuTotal, a.RamTotal); err != nil {
|
||||||
|
slog.Warn("failed to upsert vcenter_aggregate_totals", "table", ht.TableName, "snapshot_type", "hourly", "vcenter", a.Vcenter, "snapshot_time", ht.SnapshotTime, "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -162,3 +162,316 @@ func TestParseHourlySnapshotUnix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVcenterLatestTotalsAndListFallback(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
dbConn := newTestSQLiteDB(t)
|
||||||
|
|
||||||
|
if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err != nil {
|
||||||
|
t.Fatalf("failed to ensure vcenter_latest_totals: %v", err)
|
||||||
|
}
|
||||||
|
if err := InsertVcenterTotals(ctx, dbConn, "vc-a", time.Unix(200, 0), 10, 20, 30); err != nil {
|
||||||
|
t.Fatalf("failed to insert totals for vc-a: %v", err)
|
||||||
|
}
|
||||||
|
// Older snapshot should not replace latest totals.
|
||||||
|
if err := InsertVcenterTotals(ctx, dbConn, "vc-a", time.Unix(100, 0), 1, 2, 3); err != nil {
|
||||||
|
t.Fatalf("failed to insert older totals for vc-a: %v", err)
|
||||||
|
}
|
||||||
|
if err := InsertVcenterTotals(ctx, dbConn, "vc-b", time.Unix(300, 0), 11, 21, 31); err != nil {
|
||||||
|
t.Fatalf("failed to insert totals for vc-b: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vcenters, err := ListVcenters(ctx, dbConn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListVcenters failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(vcenters) != 2 || vcenters[0] != "vc-a" || vcenters[1] != "vc-b" {
|
||||||
|
t.Fatalf("unexpected vcenter list: %#v", vcenters)
|
||||||
|
}
|
||||||
|
|
||||||
|
var latest struct {
|
||||||
|
SnapshotTime int64 `db:"SnapshotTime"`
|
||||||
|
VmCount int64 `db:"VmCount"`
|
||||||
|
VcpuTotal int64 `db:"VcpuTotal"`
|
||||||
|
RamTotalGB int64 `db:"RamTotalGB"`
|
||||||
|
}
|
||||||
|
if err := dbConn.GetContext(ctx, &latest, `
|
||||||
|
SELECT "SnapshotTime","VmCount","VcpuTotal","RamTotalGB"
|
||||||
|
FROM vcenter_latest_totals
|
||||||
|
WHERE "Vcenter" = ?
|
||||||
|
`, "vc-a"); err != nil {
|
||||||
|
t.Fatalf("failed to query latest totals for vc-a: %v", err)
|
||||||
|
}
|
||||||
|
if latest.SnapshotTime != 200 || latest.VmCount != 10 || latest.VcpuTotal != 20 || latest.RamTotalGB != 30 {
|
||||||
|
t.Fatalf("unexpected latest totals for vc-a: %#v", latest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListVcenterHourlyTotalsSince(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
dbConn := newTestSQLiteDB(t)
|
||||||
|
|
||||||
|
base := time.Unix(1_700_000_000, 0)
|
||||||
|
if err := InsertVcenterTotals(ctx, dbConn, "vc-a", base.AddDate(0, 0, -60), 1, 2, 3); err != nil {
|
||||||
|
t.Fatalf("failed to insert old totals: %v", err)
|
||||||
|
}
|
||||||
|
if err := InsertVcenterTotals(ctx, dbConn, "vc-a", base.AddDate(0, 0, -10), 10, 20, 30); err != nil {
|
||||||
|
t.Fatalf("failed to insert recent totals: %v", err)
|
||||||
|
}
|
||||||
|
if err := InsertVcenterTotals(ctx, dbConn, "vc-b", base.AddDate(0, 0, -5), 100, 200, 300); err != nil {
|
||||||
|
t.Fatalf("failed to insert other-vcenter totals: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := ListVcenterHourlyTotalsSince(ctx, dbConn, "vc-a", base.AddDate(0, 0, -45))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListVcenterHourlyTotalsSince failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) != 1 {
|
||||||
|
t.Fatalf("expected 1 row for vc-a since cutoff, got %d", len(rows))
|
||||||
|
}
|
||||||
|
if rows[0].SnapshotTime != base.AddDate(0, 0, -10).Unix() || rows[0].VmCount != 10 {
|
||||||
|
t.Fatalf("unexpected row returned: %#v", rows[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInsertVcenterTotalsUpsertsHourlyAggregate(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
dbConn := newTestSQLiteDB(t)
|
||||||
|
|
||||||
|
snapshotTime := time.Unix(1_700_000_500, 0)
|
||||||
|
if err := InsertVcenterTotals(ctx, dbConn, "vc-a", snapshotTime, 12, 24, 48); err != nil {
|
||||||
|
t.Fatalf("InsertVcenterTotals failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "hourly", 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListVcenterAggregateTotals failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) != 1 {
|
||||||
|
t.Fatalf("expected 1 hourly aggregate row, got %d", len(rows))
|
||||||
|
}
|
||||||
|
if rows[0].SnapshotTime != snapshotTime.Unix() || rows[0].VmCount != 12 || rows[0].VcpuTotal != 24 || rows[0].RamTotalGB != 48 {
|
||||||
|
t.Fatalf("unexpected hourly aggregate row: %#v", rows[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListVcenterHourlyTotalsSinceUsesAggregateCache(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
dbConn := newTestSQLiteDB(t)
|
||||||
|
|
||||||
|
base := time.Unix(1_700_000_000, 0)
|
||||||
|
if err := UpsertVcenterAggregateTotal(ctx, dbConn, "hourly", "vc-a", base.Unix(), 7, 14, 21); err != nil {
|
||||||
|
t.Fatalf("UpsertVcenterAggregateTotal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := ListVcenterHourlyTotalsSince(ctx, dbConn, "vc-a", base.Add(-24*time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListVcenterHourlyTotalsSince failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) != 1 {
|
||||||
|
t.Fatalf("expected 1 cached row, got %d", len(rows))
|
||||||
|
}
|
||||||
|
if rows[0].SnapshotTime != base.Unix() || rows[0].VmCount != 7 || rows[0].VcpuTotal != 14 || rows[0].RamTotalGB != 21 {
|
||||||
|
t.Fatalf("unexpected cached hourly row: %#v", rows[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceVcenterAggregateTotalsFromSummary(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
dbConn := newTestSQLiteDB(t)
|
||||||
|
|
||||||
|
summaryTable := "inventory_daily_summary_20260101"
|
||||||
|
if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(`
|
||||||
|
CREATE TABLE %s (
|
||||||
|
"Vcenter" TEXT NOT NULL,
|
||||||
|
"Name" TEXT,
|
||||||
|
"VmId" TEXT,
|
||||||
|
"VmUuid" TEXT,
|
||||||
|
"AvgVcpuCount" REAL,
|
||||||
|
"VcpuCount" BIGINT,
|
||||||
|
"AvgRamGB" REAL,
|
||||||
|
"RamGB" BIGINT
|
||||||
|
)`, summaryTable)); err != nil {
|
||||||
|
t.Fatalf("failed to create summary table: %v", err)
|
||||||
|
}
|
||||||
|
insertSQL := fmt.Sprintf(`
|
||||||
|
INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB")
|
||||||
|
VALUES (?,?,?,?,?,?)
|
||||||
|
`, summaryTable)
|
||||||
|
rows := [][]interface{}{
|
||||||
|
{"vc-a", "vm-1", "1", "u1", 2.0, 4.0},
|
||||||
|
{"vc-a", "vm-2", "2", "u2", 3.0, 5.0},
|
||||||
|
{"vc-b", "vm-3", "3", "u3", 1.0, 2.0},
|
||||||
|
}
|
||||||
|
for _, args := range rows {
|
||||||
|
if _, err := dbConn.ExecContext(ctx, insertSQL, args...); err != nil {
|
||||||
|
t.Fatalf("failed to insert summary row: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upserted, err := ReplaceVcenterAggregateTotalsFromSummary(ctx, dbConn, summaryTable, "daily", 1_700_010_000)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReplaceVcenterAggregateTotalsFromSummary failed: %v", err)
|
||||||
|
}
|
||||||
|
if upserted != 2 {
|
||||||
|
t.Fatalf("expected 2 vcenter aggregate rows, got %d", upserted)
|
||||||
|
}
|
||||||
|
|
||||||
|
vcA, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "daily", 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListVcenterAggregateTotals(vc-a) failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(vcA) != 1 {
|
||||||
|
t.Fatalf("expected 1 vc-a daily row, got %d", len(vcA))
|
||||||
|
}
|
||||||
|
if vcA[0].SnapshotTime != 1_700_010_000 || vcA[0].VmCount != 2 || vcA[0].VcpuTotal != 5 || vcA[0].RamTotalGB != 9 {
|
||||||
|
t.Fatalf("unexpected vc-a daily aggregate row: %#v", vcA[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
vcB, err := ListVcenterAggregateTotalsSince(ctx, dbConn, "vc-b", "daily", time.Unix(1_700_009_000, 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListVcenterAggregateTotalsSince(vc-b) failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(vcB) != 1 || vcB[0].VmCount != 1 || vcB[0].VcpuTotal != 1 || vcB[0].RamTotalGB != 2 {
|
||||||
|
t.Fatalf("unexpected vc-b daily aggregate row: %#v", vcB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListVcenterTotalsByTypeDailyFallbackWarmsCache(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
dbConn := newTestSQLiteDB(t)
|
||||||
|
|
||||||
|
if _, err := dbConn.ExecContext(ctx, `
|
||||||
|
CREATE TABLE snapshot_registry (
|
||||||
|
snapshot_type TEXT,
|
||||||
|
table_name TEXT,
|
||||||
|
snapshot_time BIGINT
|
||||||
|
)`); err != nil {
|
||||||
|
t.Fatalf("failed to create snapshot_registry: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryTable := "inventory_daily_summary_20260102"
|
||||||
|
if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(`
|
||||||
|
CREATE TABLE %s (
|
||||||
|
"Vcenter" TEXT NOT NULL,
|
||||||
|
"Name" TEXT,
|
||||||
|
"VmId" TEXT,
|
||||||
|
"VmUuid" TEXT,
|
||||||
|
"AvgVcpuCount" REAL,
|
||||||
|
"VcpuCount" BIGINT,
|
||||||
|
"AvgRamGB" REAL,
|
||||||
|
"RamGB" BIGINT
|
||||||
|
)`, summaryTable)); err != nil {
|
||||||
|
t.Fatalf("failed to create summary table: %v", err)
|
||||||
|
}
|
||||||
|
insertSQL := fmt.Sprintf(`
|
||||||
|
INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB")
|
||||||
|
VALUES (?,?,?,?,?,?)
|
||||||
|
`, summaryTable)
|
||||||
|
for _, args := range [][]interface{}{
|
||||||
|
{"vc-a", "vm-1", "1", "u1", 4.0, 8.0},
|
||||||
|
{"vc-a", "vm-2", "2", "u2", 2.0, 6.0},
|
||||||
|
} {
|
||||||
|
if _, err := dbConn.ExecContext(ctx, insertSQL, args...); err != nil {
|
||||||
|
t.Fatalf("failed to insert summary row: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := dbConn.ExecContext(ctx, `INSERT INTO snapshot_registry (snapshot_type, table_name, snapshot_time) VALUES (?,?,?)`, "daily", summaryTable, int64(1_700_020_000)); err != nil {
|
||||||
|
t.Fatalf("failed to insert snapshot_registry row: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := ListVcenterTotalsByType(ctx, dbConn, "vc-a", "daily", 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListVcenterTotalsByType failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) != 1 {
|
||||||
|
t.Fatalf("expected 1 daily row, got %d", len(rows))
|
||||||
|
}
|
||||||
|
if rows[0].SnapshotTime != 1_700_020_000 || rows[0].VmCount != 2 || rows[0].VcpuTotal != 6 || rows[0].RamTotalGB != 14 {
|
||||||
|
t.Fatalf("unexpected daily totals row: %#v", rows[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
cached, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "daily", 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListVcenterAggregateTotals failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(cached) != 1 || cached[0].SnapshotTime != 1_700_020_000 || cached[0].VmCount != 2 {
|
||||||
|
t.Fatalf("expected warmed daily cache row, got %#v", cached)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncVcenterAggregateTotalsFromRegistry(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
dbConn := newTestSQLiteDB(t)
|
||||||
|
|
||||||
|
if _, err := dbConn.ExecContext(ctx, `
|
||||||
|
CREATE TABLE snapshot_registry (
|
||||||
|
snapshot_type TEXT,
|
||||||
|
table_name TEXT,
|
||||||
|
snapshot_time BIGINT
|
||||||
|
)`); err != nil {
|
||||||
|
t.Fatalf("failed to create snapshot_registry: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table1 := "inventory_daily_summary_20260103"
|
||||||
|
table2 := "inventory_daily_summary_20260104"
|
||||||
|
for _, table := range []string{table1, table2} {
|
||||||
|
if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(`
|
||||||
|
CREATE TABLE %s (
|
||||||
|
"Vcenter" TEXT NOT NULL,
|
||||||
|
"Name" TEXT,
|
||||||
|
"VmId" TEXT,
|
||||||
|
"VmUuid" TEXT,
|
||||||
|
"AvgVcpuCount" REAL,
|
||||||
|
"VcpuCount" BIGINT,
|
||||||
|
"AvgRamGB" REAL,
|
||||||
|
"RamGB" BIGINT
|
||||||
|
)`, table)); err != nil {
|
||||||
|
t.Fatalf("failed to create summary table %s: %v", table, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
insert1 := fmt.Sprintf(`INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?)`, table1)
|
||||||
|
insert2 := fmt.Sprintf(`INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?)`, table2)
|
||||||
|
for _, args := range [][]interface{}{
|
||||||
|
{"vc-a", "vm-1", "1", "u1", 2.0, 4.0},
|
||||||
|
{"vc-b", "vm-2", "2", "u2", 3.0, 5.0},
|
||||||
|
} {
|
||||||
|
if _, err := dbConn.ExecContext(ctx, insert1, args...); err != nil {
|
||||||
|
t.Fatalf("failed to insert row into %s: %v", table1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := dbConn.ExecContext(ctx, insert2, "vc-a", "vm-3", "3", "u3", 4.0, 6.0); err != nil {
|
||||||
|
t.Fatalf("failed to insert row into %s: %v", table2, err)
|
||||||
|
}
|
||||||
|
if _, err := dbConn.ExecContext(ctx, `INSERT INTO snapshot_registry (snapshot_type, table_name, snapshot_time) VALUES (?,?,?)`, "daily", table1, int64(1_700_030_000)); err != nil {
|
||||||
|
t.Fatalf("failed to insert snapshot_registry row for table1: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := dbConn.ExecContext(ctx, `INSERT INTO snapshot_registry (snapshot_type, table_name, snapshot_time) VALUES (?,?,?)`, "daily", table2, int64(1_700_040_000)); err != nil {
|
||||||
|
t.Fatalf("failed to insert snapshot_registry row for table2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotsRefreshed, rowsUpserted, err := SyncVcenterAggregateTotalsFromRegistry(ctx, dbConn, "daily")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SyncVcenterAggregateTotalsFromRegistry failed: %v", err)
|
||||||
|
}
|
||||||
|
if snapshotsRefreshed != 2 {
|
||||||
|
t.Fatalf("expected 2 snapshots refreshed, got %d", snapshotsRefreshed)
|
||||||
|
}
|
||||||
|
if rowsUpserted != 3 {
|
||||||
|
t.Fatalf("expected 3 rows upserted, got %d", rowsUpserted)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "daily", 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListVcenterAggregateTotals failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) != 2 {
|
||||||
|
t.Fatalf("expected 2 daily rows for vc-a, got %d", len(rows))
|
||||||
|
}
|
||||||
|
if rows[0].SnapshotTime != 1_700_040_000 || rows[0].VmCount != 1 || rows[0].VcpuTotal != 4 || rows[0].RamTotalGB != 6 {
|
||||||
|
t.Fatalf("unexpected latest vc-a daily row: %#v", rows[0])
|
||||||
|
}
|
||||||
|
if rows[1].SnapshotTime != 1_700_030_000 || rows[1].VmCount != 1 || rows[1].VcpuTotal != 2 || rows[1].RamTotalGB != 4 {
|
||||||
|
t.Fatalf("unexpected older vc-a daily row: %#v", rows[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -182,6 +182,11 @@ func (c *CronTask) aggregateDailySummary(ctx context.Context, targetTime time.Ti
|
|||||||
} else {
|
} else {
|
||||||
c.Logger.Debug("Registered daily snapshot", "table", summaryTable, "duration", time.Since(registerStart))
|
c.Logger.Debug("Registered daily snapshot", "table", summaryTable, "duration", time.Since(registerStart))
|
||||||
}
|
}
|
||||||
|
if refreshed, err := db.ReplaceVcenterAggregateTotalsFromSummary(ctx, dbConn, summaryTable, "daily", dayStart.Unix()); err != nil {
|
||||||
|
c.Logger.Warn("failed to refresh vcenter daily aggregate totals cache", "error", err, "table", summaryTable)
|
||||||
|
} else {
|
||||||
|
c.Logger.Debug("refreshed vcenter daily aggregate totals cache", "table", summaryTable, "rows", refreshed)
|
||||||
|
}
|
||||||
|
|
||||||
reportStart := time.Now()
|
reportStart := time.Now()
|
||||||
c.Logger.Debug("Generating daily report", "table", summaryTable)
|
c.Logger.Debug("Generating daily report", "table", summaryTable)
|
||||||
@@ -432,6 +437,11 @@ LIMIT 1
|
|||||||
} else {
|
} else {
|
||||||
c.Logger.Debug("Registered daily snapshot", "table", summaryTable, "duration", time.Since(registerStart))
|
c.Logger.Debug("Registered daily snapshot", "table", summaryTable, "duration", time.Since(registerStart))
|
||||||
}
|
}
|
||||||
|
if refreshed, err := db.ReplaceVcenterAggregateTotalsFromSummary(ctx, dbConn, summaryTable, "daily", dayStart.Unix()); err != nil {
|
||||||
|
c.Logger.Warn("failed to refresh vcenter daily aggregate totals cache", "error", err, "table", summaryTable)
|
||||||
|
} else {
|
||||||
|
c.Logger.Debug("refreshed vcenter daily aggregate totals cache", "table", summaryTable, "rows", refreshed)
|
||||||
|
}
|
||||||
reportStart := time.Now()
|
reportStart := time.Now()
|
||||||
c.Logger.Debug("Generating daily report", "table", summaryTable)
|
c.Logger.Debug("Generating daily report", "table", summaryTable)
|
||||||
if err := c.generateReport(ctx, summaryTable); err != nil {
|
if err := c.generateReport(ctx, summaryTable); err != nil {
|
||||||
|
|||||||
56
main.go
56
main.go
@@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"vctp/db"
|
"vctp/db"
|
||||||
|
"vctp/internal/report"
|
||||||
"vctp/internal/secrets"
|
"vctp/internal/secrets"
|
||||||
"vctp/internal/settings"
|
"vctp/internal/settings"
|
||||||
"vctp/internal/tasks"
|
"vctp/internal/tasks"
|
||||||
@@ -42,6 +43,7 @@ func main() {
|
|||||||
settingsPath := flag.String("settings", "/etc/dtms/vctp.yml", "Path to settings YAML")
|
settingsPath := flag.String("settings", "/etc/dtms/vctp.yml", "Path to settings YAML")
|
||||||
runInventory := flag.Bool("run-inventory", false, "Run a single inventory snapshot across all configured vCenters and exit")
|
runInventory := flag.Bool("run-inventory", false, "Run a single inventory snapshot across all configured vCenters and exit")
|
||||||
dbCleanup := flag.Bool("db-cleanup", false, "Run a one-time cleanup to drop low-value hourly snapshot indexes and exit")
|
dbCleanup := flag.Bool("db-cleanup", false, "Run a one-time cleanup to drop low-value hourly snapshot indexes and exit")
|
||||||
|
backfillVcenterCache := flag.Bool("backfill-vcenter-cache", false, "Run a one-time backfill for vcenter latest+aggregate cache tables and exit")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
bootstrapLogger := log.New(log.LevelInfo, log.OutputText)
|
bootstrapLogger := log.New(log.LevelInfo, log.OutputText)
|
||||||
@@ -104,6 +106,60 @@ func main() {
|
|||||||
logger.Info("completed hourly snapshot index cleanup", "indexes_dropped", dropped)
|
logger.Info("completed hourly snapshot index cleanup", "indexes_dropped", dropped)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if *backfillVcenterCache {
|
||||||
|
logger.Info("starting one-time vcenter cache backfill")
|
||||||
|
if err := report.EnsureSnapshotRegistry(ctx, database); err != nil {
|
||||||
|
logger.Error("failed to ensure snapshot registry", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
hourlyRecords, err := report.ListSnapshots(ctx, database, "hourly")
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to list hourly snapshots from registry", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if len(hourlyRecords) == 0 {
|
||||||
|
logger.Warn("snapshot registry has no hourly entries; attempting registry migration before cache backfill")
|
||||||
|
stats, err := report.MigrateSnapshotRegistry(ctx, database)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to migrate snapshot registry before cache backfill", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
logger.Info("snapshot registry migration complete",
|
||||||
|
"hourly_renamed", stats.HourlyRenamed,
|
||||||
|
"hourly_registered", stats.HourlyRegistered,
|
||||||
|
"daily_registered", stats.DailyRegistered,
|
||||||
|
"monthly_registered", stats.MonthlyRegistered,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.SyncVcenterTotalsFromSnapshots(ctx, database.DB()); err != nil {
|
||||||
|
logger.Error("failed to backfill hourly vcenter totals cache", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
latestSynced, err := db.SyncVcenterLatestTotalsFromHistory(ctx, database.DB())
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to backfill latest vcenter totals cache", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dailySnapshots, dailyRows, dailyErr := db.SyncVcenterAggregateTotalsFromRegistry(ctx, database.DB(), "daily")
|
||||||
|
if dailyErr != nil {
|
||||||
|
logger.Warn("daily vcenter aggregate cache backfill completed with warnings", "error", dailyErr)
|
||||||
|
}
|
||||||
|
monthlySnapshots, monthlyRows, monthlyErr := db.SyncVcenterAggregateTotalsFromRegistry(ctx, database.DB(), "monthly")
|
||||||
|
if monthlyErr != nil {
|
||||||
|
logger.Warn("monthly vcenter aggregate cache backfill completed with warnings", "error", monthlyErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("completed one-time vcenter cache backfill",
|
||||||
|
"latest_rows_synced", latestSynced,
|
||||||
|
"daily_snapshots_refreshed", dailySnapshots,
|
||||||
|
"daily_rows_upserted", dailyRows,
|
||||||
|
"monthly_snapshots_refreshed", monthlySnapshots,
|
||||||
|
"monthly_rows_upserted", monthlyRows,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Determine bind IP
|
// Determine bind IP
|
||||||
bindIP := strings.TrimSpace(s.Values.Settings.BindIP)
|
bindIP := strings.TrimSpace(s.Values.Settings.BindIP)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -11,6 +12,12 @@ import (
|
|||||||
"vctp/db"
|
"vctp/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
vcenterHourlyDetailWindowDays = 45
|
||||||
|
vcenterDailyDefaultLimit = 400
|
||||||
|
vcenterMonthlyDefaultLimit = 200
|
||||||
|
)
|
||||||
|
|
||||||
// VcenterList renders a list of vCenters being monitored.
|
// VcenterList renders a list of vCenters being monitored.
|
||||||
// @Summary List vCenters
|
// @Summary List vCenters
|
||||||
// @Description Lists all vCenters with recorded snapshot totals.
|
// @Description Lists all vCenters with recorded snapshot totals.
|
||||||
@@ -20,9 +27,6 @@ import (
|
|||||||
// @Router /vcenters [get]
|
// @Router /vcenters [get]
|
||||||
func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil {
|
|
||||||
h.Logger.Warn("failed to sync vcenter totals", "error", err)
|
|
||||||
}
|
|
||||||
vcs, err := db.ListVcenters(ctx, h.Database.DB())
|
vcs, err := db.ListVcenters(ctx, h.Database.DB())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("failed to list vcenters: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("failed to list vcenters: %v", err), http.StatusInternalServerError)
|
||||||
@@ -32,7 +36,7 @@ func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
|
|||||||
for _, vc := range vcs {
|
for _, vc := range vcs {
|
||||||
links = append(links, views.VcenterLink{
|
links = append(links, views.VcenterLink{
|
||||||
Name: vc,
|
Name: vc,
|
||||||
Link: "/vcenters/totals?vcenter=" + url.QueryEscape(vc),
|
Link: "/vcenters/totals/daily?vcenter=" + url.QueryEscape(vc),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
@@ -41,49 +45,117 @@ func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VcenterTotals renders totals for a vCenter.
|
// VcenterTotals keeps backward compatibility with the original endpoint and routes to the new pages.
|
||||||
// @Summary vCenter totals
|
// @Summary vCenter totals
|
||||||
// @Description Shows per-snapshot totals for a vCenter.
|
// @Description Redirect-style handler for compatibility; use /vcenters/totals/daily or /vcenters/totals/hourly.
|
||||||
// @Tags vcenters
|
// @Tags vcenters
|
||||||
// @Produce text/html
|
// @Produce text/html
|
||||||
// @Param vcenter query string true "vCenter URL"
|
// @Param vcenter query string true "vCenter URL"
|
||||||
// @Param type query string false "hourly|daily|monthly (default: hourly)"
|
// @Param type query string false "hourly|daily|monthly"
|
||||||
// @Param limit query int false "Limit results (default 200)"
|
// @Param limit query int false "Limit results"
|
||||||
// @Success 200 {string} string "HTML page"
|
// @Success 200 {string} string "HTML page"
|
||||||
// @Failure 400 {string} string "Missing vcenter"
|
// @Failure 400 {string} string "Missing vcenter"
|
||||||
// @Router /vcenters/totals [get]
|
// @Router /vcenters/totals [get]
|
||||||
func (h *Handler) VcenterTotals(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) VcenterTotals(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type"))) {
|
||||||
|
case "hourly", "hourly-detail", "detail", "detailed":
|
||||||
|
h.VcenterTotalsHourlyDetailed(w, r)
|
||||||
|
return
|
||||||
|
case "monthly":
|
||||||
|
h.vcenterTotalsLegacyMonthly(w, r)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
h.VcenterTotalsDaily(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VcenterTotalsDaily renders the daily-aggregation totals page for one vCenter.
|
||||||
|
// @Summary vCenter daily totals
|
||||||
|
// @Description Shows daily aggregated VM count/vCPU/RAM totals for a vCenter.
|
||||||
|
// @Tags vcenters
|
||||||
|
// @Produce text/html
|
||||||
|
// @Param vcenter query string true "vCenter URL"
|
||||||
|
// @Param limit query int false "Limit results (default 400)"
|
||||||
|
// @Success 200 {string} string "HTML page"
|
||||||
|
// @Failure 400 {string} string "Missing vcenter"
|
||||||
|
// @Router /vcenters/totals/daily [get]
|
||||||
|
func (h *Handler) VcenterTotalsDaily(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
vc := r.URL.Query().Get("vcenter")
|
vc, ok := requiredVcenterParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
limit := parsePositiveLimit(r, vcenterDailyDefaultLimit)
|
||||||
|
rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, "daily", limit)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to list daily totals: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.renderVcenterTotalsPage(ctx, w, vc, "daily", rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VcenterTotalsHourlyDetailed renders a detailed hourly page over the most recent 45 days.
|
||||||
|
// @Summary vCenter hourly totals (45 days)
|
||||||
|
// @Description Shows detailed hourly VM count/vCPU/RAM totals for the latest 45 days.
|
||||||
|
// @Tags vcenters
|
||||||
|
// @Produce text/html
|
||||||
|
// @Param vcenter query string true "vCenter URL"
|
||||||
|
// @Success 200 {string} string "HTML page"
|
||||||
|
// @Failure 400 {string} string "Missing vcenter"
|
||||||
|
// @Router /vcenters/totals/hourly [get]
|
||||||
|
func (h *Handler) VcenterTotalsHourlyDetailed(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
vc, ok := requiredVcenterParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
since := time.Now().AddDate(0, 0, -vcenterHourlyDetailWindowDays)
|
||||||
|
rows, err := db.ListVcenterHourlyTotalsSince(ctx, h.Database.DB(), vc, since)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to list hourly totals: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.renderVcenterTotalsPage(ctx, w, vc, "hourly45", rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) vcenterTotalsLegacyMonthly(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
vc, ok := requiredVcenterParam(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
limit := parsePositiveLimit(r, vcenterMonthlyDefaultLimit)
|
||||||
|
rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, "monthly", limit)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to list monthly totals: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.renderVcenterTotalsPage(ctx, w, vc, "monthly", rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requiredVcenterParam(w http.ResponseWriter, r *http.Request) (string, bool) {
|
||||||
|
vc := strings.TrimSpace(r.URL.Query().Get("vcenter"))
|
||||||
if vc == "" {
|
if vc == "" {
|
||||||
http.Error(w, "vcenter is required", http.StatusBadRequest)
|
http.Error(w, "vcenter is required", http.StatusBadRequest)
|
||||||
return
|
return "", false
|
||||||
}
|
}
|
||||||
viewType := strings.ToLower(r.URL.Query().Get("type"))
|
return vc, true
|
||||||
if viewType == "" {
|
|
||||||
viewType = "hourly"
|
|
||||||
}
|
}
|
||||||
switch viewType {
|
|
||||||
case "hourly", "daily", "monthly":
|
func parsePositiveLimit(r *http.Request, defaultLimit int) int {
|
||||||
default:
|
if defaultLimit <= 0 {
|
||||||
viewType = "hourly"
|
defaultLimit = 200
|
||||||
}
|
}
|
||||||
if viewType == "hourly" {
|
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
||||||
if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil {
|
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
|
||||||
h.Logger.Warn("failed to sync vcenter totals", "error", err)
|
return parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
limit := 200
|
return defaultLimit
|
||||||
if l := r.URL.Query().Get("limit"); l != "" {
|
|
||||||
if v, err := strconv.Atoi(l); err == nil && v > 0 {
|
|
||||||
limit = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, viewType, limit)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, fmt.Sprintf("failed to list totals: %v", err), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) renderVcenterTotalsPage(ctx context.Context, w http.ResponseWriter, vc string, viewType string, rows []db.VcenterTotalRow) {
|
||||||
entries := make([]views.VcenterTotalsEntry, 0, len(rows))
|
entries := make([]views.VcenterTotalsEntry, 0, len(rows))
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
entries = append(entries, views.VcenterTotalsEntry{
|
entries = append(entries, views.VcenterTotalsEntry{
|
||||||
@@ -105,28 +177,26 @@ func (h *Handler) VcenterTotals(w http.ResponseWriter, r *http.Request) {
|
|||||||
func buildVcenterMeta(vcenter string, viewType string) views.VcenterTotalsMeta {
|
func buildVcenterMeta(vcenter string, viewType string) views.VcenterTotalsMeta {
|
||||||
active := viewType
|
active := viewType
|
||||||
if active == "" {
|
if active == "" {
|
||||||
active = "hourly"
|
active = "daily"
|
||||||
}
|
}
|
||||||
meta := views.VcenterTotalsMeta{
|
meta := views.VcenterTotalsMeta{
|
||||||
ViewType: active,
|
ViewType: active,
|
||||||
TypeLabel: "Hourly",
|
TypeLabel: "Daily",
|
||||||
HourlyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=hourly",
|
HourlyLink: "/vcenters/totals/hourly?vcenter=" + url.QueryEscape(vcenter),
|
||||||
DailyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=daily",
|
DailyLink: "/vcenters/totals/daily?vcenter=" + url.QueryEscape(vcenter),
|
||||||
MonthlyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=monthly",
|
|
||||||
HourlyClass: "web3-button",
|
HourlyClass: "web3-button",
|
||||||
DailyClass: "web3-button",
|
DailyClass: "web3-button",
|
||||||
MonthlyClass: "web3-button",
|
|
||||||
}
|
}
|
||||||
switch active {
|
switch active {
|
||||||
case "daily":
|
case "hourly45", "hourly":
|
||||||
meta.TypeLabel = "Daily"
|
meta.ViewType = "hourly45"
|
||||||
meta.DailyClass = "web3-button active"
|
meta.TypeLabel = fmt.Sprintf("Hourly (last %d days)", vcenterHourlyDetailWindowDays)
|
||||||
|
meta.HourlyClass = "web3-button active"
|
||||||
case "monthly":
|
case "monthly":
|
||||||
meta.TypeLabel = "Monthly"
|
meta.TypeLabel = "Monthly"
|
||||||
meta.MonthlyClass = "web3-button active"
|
|
||||||
default:
|
default:
|
||||||
meta.ViewType = "hourly"
|
meta.ViewType = "daily"
|
||||||
meta.HourlyClass = "web3-button active"
|
meta.DailyClass = "web3-button active"
|
||||||
}
|
}
|
||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
|
|||||||
mux.HandleFunc("/vm/trace", h.VmTrace)
|
mux.HandleFunc("/vm/trace", h.VmTrace)
|
||||||
mux.HandleFunc("/vcenters", h.VcenterList)
|
mux.HandleFunc("/vcenters", h.VcenterList)
|
||||||
mux.HandleFunc("/vcenters/totals", h.VcenterTotals)
|
mux.HandleFunc("/vcenters/totals", h.VcenterTotals)
|
||||||
|
mux.HandleFunc("/vcenters/totals/daily", h.VcenterTotalsDaily)
|
||||||
|
mux.HandleFunc("/vcenters/totals/hourly", h.VcenterTotalsHourlyDetailed)
|
||||||
mux.HandleFunc("/metrics", h.Metrics)
|
mux.HandleFunc("/metrics", h.Metrics)
|
||||||
|
|
||||||
mux.HandleFunc("/snapshots/hourly", h.SnapshotHourlyList)
|
mux.HandleFunc("/snapshots/hourly", h.SnapshotHourlyList)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: "vctp"
|
name: "vctp"
|
||||||
arch: "amd64"
|
arch: "amd64"
|
||||||
platform: "linux"
|
platform: "linux"
|
||||||
version: "v26.1.2"
|
version: "v26.1.3"
|
||||||
version_schema: semver
|
version_schema: semver
|
||||||
description: vCTP monitors VMware VM inventory and event data to build chargeback reports
|
description: vCTP monitors VMware VM inventory and event data to build chargeback reports
|
||||||
maintainer: "@coadn"
|
maintainer: "@coadn"
|
||||||
@@ -13,7 +13,7 @@ contents:
|
|||||||
- src: build/vctp-linux-amd64
|
- src: build/vctp-linux-amd64
|
||||||
dst: /usr/bin/vctp-linux-amd64
|
dst: /usr/bin/vctp-linux-amd64
|
||||||
|
|
||||||
- src: src/vctp.yml
|
- src: src/vctp-service.yml
|
||||||
dst: /etc/dtms/vctp.yml
|
dst: /etc/dtms/vctp.yml
|
||||||
type: config|noreplace
|
type: config|noreplace
|
||||||
file_info:
|
file_info:
|
||||||
Reference in New Issue
Block a user