Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions counter/counter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Package counter, provides a means of incrementing a named counter.
package counter

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
)

// IncrementNamedCounter takes a key, an increment value, and a JSON file path, and updates the value in the file
func IncrementNamedCounter(key string, increment int, filename string) (int, error) {
jsonData, err := loadJSONFile(filename)
if err != nil {
return 0, err
}

existingValue, ok := jsonData[key]
if !ok {
jsonData[key] = increment
} else {
switch v := existingValue.(type) {
case float64:
jsonData[key] = v + float64(increment)
case int64:
jsonData[key] = v + int64(increment)
case int32:
jsonData[key] = v + int32(increment)
case int:
jsonData[key] = v + increment
default:
return 0, fmt.Errorf("value for key '%s' is not a valid numeric type", key)
}
}

err = writeJSONFile(filename, jsonData)
if err != nil {
return 0, err
}

value, ok := jsonData[key]
if !ok {
return 1, nil
}

switch v := value.(type) {
case float64:
return int(v), nil
case int64:
return int(v), nil
case int:
return int(v), nil
default:
return 0, fmt.Errorf("value for key '%s' is not a valid int32", key)
}
}

// writeJSONFile writes the map into a JSON file
func writeJSONFile(filename string, jsonData map[string]interface{}) error {
updatedData, err := json.MarshalIndent(jsonData, "", " ")
if err != nil {
return err
}

err = ioutil.WriteFile(filename, updatedData, 0644)
if err != nil {
return err
}

return nil
}

// loadJSONFile reads the contents of a JSON file and returns a map[string]interface{}
func loadJSONFile(filename string) (map[string]interface{}, error) {

if _, err := os.Stat(filename); os.IsNotExist(err) {
err := ioutil.WriteFile(filename, []byte("{}"), 0644)
if err != nil {
return nil, fmt.Errorf("failed to create file: %w", err)
}
}

data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}

var jsonData map[string]interface{}
err = json.Unmarshal(data, &jsonData)
if err != nil {
return nil, err
}

return jsonData, nil
}
27 changes: 27 additions & 0 deletions counter/counter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package counter

import (
"path/filepath"
"testing"
)

func TestThatANoneExistentCounterWillBeCreatedAndIncrements(t *testing.T) {
tempDir := t.TempDir()
testFile := filepath.Join(tempDir, "test.json")

counter, err := IncrementNamedCounter("testCounter", 1, testFile)
if err != nil {
t.Errorf("Not expecting and err to be returned: %v", err)
}
if counter != 1 {
t.Errorf("Counter expected to be 1 but got %d", counter)
}

counter, err = IncrementNamedCounter("testCounter", 1, testFile)
if err != nil {
t.Errorf("Not expecting and err to be returned: %v", err)
}
if counter != 2 {
t.Errorf("Counter expected to be 2 but got %d", counter)
}
}
38 changes: 30 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@ import (
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"syscall"
"time"

"github.com/meysam81/prometheus-command-timer/counter"
)

type Config struct {
PushgatewayURL string
JobName string
InstanceName string
Labels string
Debug bool
Version bool
Info bool
PushgatewayURL string
JobName string
InstanceName string
Labels string
ExecCountTransientStore string
Debug bool
Version bool
Info bool
}

func main() {
Expand All @@ -32,6 +37,7 @@ func main() {
flag.StringVar(&config.JobName, "job-name", "", "Job name for metrics (required)")
flag.StringVar(&config.InstanceName, "instance-name", config.InstanceName, "Instance name for metrics")
flag.StringVar(&config.Labels, "labels", "", "Additional labels in key=value format, comma-separated (e.g., env=prod,team=infra)")
flag.StringVar(&config.ExecCountTransientStore, "execution-count-store", filepath.Join(os.TempDir(), "prometheus-command-time.json"), "Override the default transient store filename (<tmp>/prometheus-command-time.json)")
flag.BoolVar(&config.Version, "version", false, "Output version")
showHelp := flag.Bool("help", false, "Show help message")
flag.BoolVar(showHelp, "h", false, "Show help message (shorthand)")
Expand Down Expand Up @@ -133,11 +139,27 @@ func executeCommand(config *Config, cmdArgs []string) int {
sendMetric(config, "job_duration_seconds", fmt.Sprintf("%.6f", duration), "gauge", "Total time taken for job execution in seconds")
sendMetric(config, "job_exit_status", fmt.Sprintf("%d", exitStatus), "gauge", "Exit status code of the last job execution (0=success)")
sendMetric(config, "job_last_execution_timestamp", fmt.Sprintf("%d", endTime), "gauge", "Timestamp of the last job execution")
sendMetric(config, "job_executions_total", "1", "counter", "Total number of job executions")
sendMetric(config, "job_executions_total", strconv.Itoa(incrementExecutionCounter(config)), "counter", "Total number of job executions")

return exitStatus
}

// incrementExecutionCounter will return the next value of a counter which
// is named using the push gateway URL.
func incrementExecutionCounter(config *Config) int {
counterVal := 1
counterName, err := buildPushgatewayURL(config)
if err != nil {
logStdout(config, "error building counter name: %v", err)
} else {
counterVal, err = counter.IncrementNamedCounter(counterName, 1, config.ExecCountTransientStore)
if err != nil {
logStdout(config, "error loading counter: %v", err)
}
}
return counterVal
}

func buildPushgatewayURL(config *Config) (string, error) {
url := fmt.Sprintf("%s/metrics/job/%s/instance/%s",
strings.TrimSuffix(config.PushgatewayURL, "/"),
Expand Down
Loading