Skip to content

Commit

Permalink
feat(api): add /api/v1/counts endpoint - app stats
Browse files Browse the repository at this point in the history
- service_count
- updates_available
- updates_skipped

fixes #436
  • Loading branch information
JosephKav committed Jan 13, 2025
1 parent d5fb0f4 commit a2e0565
Show file tree
Hide file tree
Showing 44 changed files with 1,900 additions and 1,179 deletions.
5 changes: 3 additions & 2 deletions cmd/argus/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,11 @@ func main() {
}
}

go db.Run(&config, &jLog)
// Setup DB and last known service versions.
db.Run(&config, &jLog)

// Track all targets for changes in version and act on any found changes.
go (&config).Service.Track(&config.Order, &config.OrderMutex)
go config.Service.Track(&config.Order, &config.OrderMutex)

// Web server.
web.Run(&config, &jLog)
Expand Down
4 changes: 2 additions & 2 deletions command/command.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright [2024] [Argus]
// Copyright [2025] [Argus]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -90,7 +90,7 @@ func (c *Controller) ExecIndex(logFrom util.LogFrom, index int) error {
util.ServiceInfo{ID: *c.ServiceStatus.ServiceID},
true)
}
metric.IncreasePrometheusCounter(metric.CommandMetric,
metric.IncPrometheusCounter(metric.CommandResultTotal,
(*c.Command)[index].String(),
*c.ServiceStatus.ServiceID,
"",
Expand Down
10 changes: 5 additions & 5 deletions command/init.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright [2024] [Argus]
// Copyright [2025] [Argus]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -70,12 +70,12 @@ func (c *Controller) InitMetrics() {
// ############
for _, cmd := range *c.Command {
name := cmd.String()
metric.InitPrometheusCounter(metric.CommandMetric,
metric.InitPrometheusCounter(metric.CommandResultTotal,
name,
*c.ServiceStatus.ServiceID,
"",
"SUCCESS")
metric.InitPrometheusCounter(metric.CommandMetric,
metric.InitPrometheusCounter(metric.CommandResultTotal,
name,
*c.ServiceStatus.ServiceID,
"",
Expand All @@ -91,12 +91,12 @@ func (c *Controller) DeleteMetrics() {

for _, cmd := range *c.Command {
name := cmd.String()
metric.DeletePrometheusCounter(metric.CommandMetric,
metric.DeletePrometheusCounter(metric.CommandResultTotal,
name,
*c.ServiceStatus.ServiceID,
"",
"SUCCESS")
metric.DeletePrometheusCounter(metric.CommandMetric,
metric.DeletePrometheusCounter(metric.CommandResultTotal,
name,
*c.ServiceStatus.ServiceID,
"",
Expand Down
12 changes: 6 additions & 6 deletions command/init_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright [2024] [Argus]
// Copyright [2025] [Argus]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -376,13 +376,13 @@ func TestController_Metrics(t *testing.T) {
}

// WHEN the Prometheus metrics are initialised with initMetrics
hadC := testutil.CollectAndCount(metric.CommandMetric)
hadC := testutil.CollectAndCount(metric.CommandResultTotal)
hadG := testutil.CollectAndCount(metric.LatestVersionIsDeployed)
controller.InitMetrics()

// THEN it can be collected
// counters
gotC := testutil.CollectAndCount(metric.CommandMetric)
gotC := testutil.CollectAndCount(metric.CommandResultTotal)
wantC := 2 * len(*controller.Command)
if (gotC - hadC) != wantC {
t.Errorf("Controller.InitMetrics() %d Counter metrics were initialised, expecting %d",
Expand All @@ -399,7 +399,7 @@ func TestController_Metrics(t *testing.T) {
// AND it can be deleted
// counters
controller.DeleteMetrics()
gotC = testutil.CollectAndCount(metric.CommandMetric)
gotC = testutil.CollectAndCount(metric.CommandResultTotal)
if gotC != hadC {
t.Errorf("Controller.DeleteMetrics() was called, but Counter metrics mismatch got %d, expecting %d",
gotC, hadC)
Expand All @@ -416,7 +416,7 @@ func TestController_Metrics(t *testing.T) {
// InitMetrics
controller.InitMetrics()
// counters
gotC = testutil.CollectAndCount(metric.CommandMetric)
gotC = testutil.CollectAndCount(metric.CommandResultTotal)
if gotC != hadC {
t.Errorf("Controller.InitMetrics() on nil Controller shouldn't have changed the Counter metrics, got %d. expecting %d",
gotC, hadC)
Expand All @@ -430,7 +430,7 @@ func TestController_Metrics(t *testing.T) {
// DeleteMetrics
controller.DeleteMetrics()
// counters
gotC = testutil.CollectAndCount(metric.CommandMetric)
gotC = testutil.CollectAndCount(metric.CommandResultTotal)
if gotC != hadC {
t.Errorf("Controller.DeleteMetrics() on nil Controller shouldn't have changed the Counter metrics, got %d. expecting %d",
gotC, hadC)
Expand Down
5 changes: 3 additions & 2 deletions db/handlers.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright [2024] [Argus]
// Copyright [2025] [Argus]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -23,8 +23,9 @@ import (
)

// handler will listen to the DatabaseChannel and act on
// incoming messages.
// incoming messages to the DatabaseChannel.
func (api *api) handler() {
defer api.db.Close()
for message := range *api.config.DatabaseChannel {
// If the message is to delete a row.
if message.Delete {
Expand Down
46 changes: 23 additions & 23 deletions db/handlers_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright [2024] [Argus]
// Copyright [2025] [Argus]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,7 +27,7 @@ import (
)

func TestAPI_UpdateRow(t *testing.T) {
// GIVEN a DB with a few service status'
// GIVEN a DB with a few service status'.
tests := map[string]struct {
cells []dbtype.Cell
target string
Expand Down Expand Up @@ -107,21 +107,21 @@ func TestAPI_UpdateRow(t *testing.T) {
t.Cleanup(func() { dbCleanup(tAPI) })
tAPI.initialise()

// Ensure the row exists if tc.exists
// Ensure the row exists when tc.exists.
if tc.exists {
tAPI.db.Exec("INSERT INTO status (id) VALUES (?)",
tc.target)
}
// Delete the DB file
// Delete the DB file.
if tc.databaseDeleted {
os.Remove(tAPI.config.Settings.Data.DatabaseFile)
}

// WHEN updateRow is called targeting single/multiple cells
// WHEN updateRow is called targeting single/multiple cells.
tAPI.updateRow(tc.target, tc.cells)
time.Sleep(100 * time.Millisecond)

// THEN those cell(s) are changed in the DB
// THEN those cell(s) are changed in the DB.
row := queryRow(t, tAPI.db, tc.target)
for _, cell := range tc.cells {
var got string
Expand All @@ -137,7 +137,7 @@ func TestAPI_UpdateRow(t *testing.T) {
case "approved_version":
got = row.ApprovedVersion()
default:
continue // Skip unknown columns
continue // Skip unknown columns.
}
if got != cell.Value {
if !tc.databaseDeleted {
Expand All @@ -155,7 +155,7 @@ func TestAPI_UpdateRow(t *testing.T) {
}

func TestAPI_DeleteRow(t *testing.T) {
// GIVEN a DB with a few service status'
// GIVEN a DB with a few service status'.
tests := map[string]struct {
serviceID string
exists bool
Expand All @@ -175,15 +175,15 @@ func TestAPI_DeleteRow(t *testing.T) {

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// t.Parallel() - Cannot run in parallel since we're using stdout
// t.Parallel() - Cannot run in parallel since we're using stdout.
releaseStdout := test.CaptureStdout()

tAPI := testAPI(name, "TestAPI_DeleteRow")
os.Remove(tAPI.config.Settings.Data.DatabaseFile)
t.Cleanup(func() { dbCleanup(tAPI) })
tAPI.initialise()

// Ensure the row exists if tc.exists
// Ensure the row exists if tc.exists.
if tc.exists {
tAPI.updateRow(
tc.serviceID,
Expand All @@ -192,31 +192,31 @@ func TestAPI_DeleteRow(t *testing.T) {
)
time.Sleep(100 * time.Millisecond)
}
// Check the row existence before the test
// Check the row existence before the test.
row := queryRow(t, tAPI.db, tc.serviceID)
if tc.exists && (row.LatestVersion() == "" || row.DeployedVersion() == "") {
t.Errorf("expecting row to exist. got %#v", row)
}
// Delete the DB file
// Delete the DB file.
if tc.databaseDeleted {
os.Remove(tAPI.config.Settings.Data.DatabaseFile)
}

// WHEN deleteRow is called targeting a row
// WHEN deleteRow is called targeting a row.
tAPI.deleteRow(tc.serviceID)
time.Sleep(100 * time.Millisecond)

// THEN if we deleted the DB before the statement, we should have logged an error
// THEN if we deleted the DB before the statement, we should have logged an error.
stdout := releaseStdout()
deleteFailRegex := `ERROR: [^)]+\), deleteRow`
if tc.databaseDeleted != util.RegexCheck(deleteFailRegex, stdout) {
t.Errorf("stdout mismatch:\nwant=%t (%q)\ngot:\n%q",
tc.databaseDeleted, deleteFailRegex, stdout)
}
// AND the row is deleted from the DB (if it existed and the DB wasn't deleted)
// AND the row is deleted from the DB (if it existed and the DB wasn't deleted).
row = queryRow(t, tAPI.db, tc.serviceID)
if row.LatestVersion() != "" || row.DeployedVersion() != "" {
// no delete if we deleted the db
// no delete if we deleted the db.
if !tc.databaseDeleted {
t.Errorf("expecting row to be deleted.\ngot %#v", row)
}
Expand All @@ -228,13 +228,13 @@ func TestAPI_DeleteRow(t *testing.T) {
}

func TestAPI_Handler(t *testing.T) {
// GIVEN a DB with a few service status'
// GIVEN a DB with a few service status'.
tAPI := testAPI("TestAPI_Handler", "db")
t.Cleanup(func() { dbCleanup(tAPI) })
tAPI.initialise()
go tAPI.handler()

// WHEN a message is sent to the DatabaseChannel targeting latest_version
// WHEN a message is sent to the DatabaseChannel targeting latest_version.
target := "keep0"
cell1 := dbtype.Cell{
Column: "latest_version", Value: "9.9.9"}
Expand All @@ -253,7 +253,7 @@ func TestAPI_Handler(t *testing.T) {
*tAPI.config.DatabaseChannel <- msg1
time.Sleep(250 * time.Millisecond)

// THEN the cell was changed in the DB
// THEN the cell was changed in the DB.
got := queryRow(t, tAPI.db, target)
if got.LatestVersion() != want.LatestVersion() {
t.Errorf("Expected %q to be updated to %q\ngot %#v\nwant %#v",
Expand All @@ -262,28 +262,28 @@ func TestAPI_Handler(t *testing.T) {

// ------------------------------

// WHEN a message is sent to the DatabaseChannel deleting a row
// WHEN a message is sent to the DatabaseChannel deleting a row.
*tAPI.config.DatabaseChannel <- dbtype.Message{
ServiceID: target,
Delete: true,
}
time.Sleep(250 * time.Millisecond)

// THEN the row is deleted from the DB
// THEN the row is deleted from the DB.
got = queryRow(t, tAPI.db, target)
if got.LatestVersion() != "" || got.DeployedVersion() != "" {
t.Errorf("Expected row to be deleted\ngot %#v\nwant %#v", got, want)
}

// ------------------------------

// WHEN multiple messages are targeting the same row in quick succession
// WHEN multiple messages are targeting the same row in quick succession.
*tAPI.config.DatabaseChannel <- msg1
wantLatestVersion := msg2.Cells[0].Value
*tAPI.config.DatabaseChannel <- msg2
time.Sleep(250 * time.Millisecond)

// THEN the last message is the one that is applied
// THEN the last message is the one that is applied.
got = queryRow(t, tAPI.db, target)
if got.LatestVersion() != wantLatestVersion {
t.Errorf("Expected %q to be updated to %q\ngot %#v\nwant %#v",
Expand Down
11 changes: 5 additions & 6 deletions db/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,14 @@ var cfg *config.Config
func TestMain(m *testing.M) {
databaseFile := "TestDB.db"

// Log
// Log.
jLog := util.NewJLog("DEBUG", false)
jLog.Testing = true
LogInit(jLog, databaseFile)

cfg = testConfig()
cfg.Settings.Data.DatabaseFile = databaseFile
go Run(cfg, nil)
time.Sleep(250 * time.Millisecond) // Time for db to start
Run(cfg, nil)

exitCode := m.Run()
os.Remove(cfg.Settings.Data.DatabaseFile)
Expand Down Expand Up @@ -85,7 +84,7 @@ func testConfig() (cfg *config.Config) {
SaveChannel: &saveChannel,
}

// Services
// Services.
for svcName := range cfg.Service {
svc := service.Service{
ID: "foo",
Expand All @@ -99,7 +98,7 @@ func testConfig() (cfg *config.Config) {
svc.Status.SetDeployedVersion("2.0.0", "", false)
svc.Status.SetLatestVersion("3.0.0", time.Now().Add(time.Hour).Format(time.RFC3339), false)

// Add service to Config
// Add service to Config.
cfg.Service[svcName] = &svc
}

Expand Down Expand Up @@ -137,7 +136,7 @@ func queryRow(t *testing.T, db *sql.DB, serviceID string) *status.Status {
approved_version
FROM status
WHERE id = ?;`
// Retry up-to 10 times incase 'database is locked'
// Retry up-to 10 times in case 'database is locked'.
var row *sql.Rows
var err error
for i := 0; i < 10; i++ {
Expand Down
18 changes: 13 additions & 5 deletions db/init.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright [2024] [Argus]
// Copyright [2025] [Argus]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -75,20 +75,28 @@ func checkFile(path string) {
}
}

// Run the database, initialising it and running the handler for any new message(s).
// Run will start the database, initialise it and run the handler for messages in the background.
func Run(cfg *config.Config, log *util.JLog) {
api := api{config: cfg}
if log != nil {
LogInit(log, cfg.Settings.DataDatabaseFile())
}
api := api{config: cfg}

api.initialise()
defer api.db.Close()
runningHandler := false
defer func() {
if !runningHandler {
api.db.Close()
}
}()

if len(api.config.Order) > 0 {
api.removeUnknownServices()
api.extractServiceStatus()
}

api.handler()
go api.handler()
runningHandler = true
}

func (api *api) initialise() {
Expand Down
Loading

0 comments on commit a2e0565

Please sign in to comment.