1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
package main
import (
"flag"
"fmt"
"io/fs"
"log/slog"
"maps"
"net/http"
"os"
"path/filepath"
"slices"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
httpHost string
httpPort int
interval int
namespace string
path string
subsystem string
customLabels MapVar
extensionKeys map[string]bool
gauges map[string]*prometheus.GaugeVec
)
func main() {
// Set options
flag.StringVar(&httpHost, "host", "[::]", "The path to export metrics of")
flag.IntVar(&httpPort, "port", 9091, "The path to export metrics of")
flag.IntVar(&interval, "interval", 10, "The path to export metrics of")
flag.StringVar(&namespace, "namespace", "fsdir_exporter", "Namespace to use for the Prometheus metrics")
flag.StringVar(&path, "path", "", "The path to export metrics of")
flag.StringVar(&subsystem, "subsystem", "", "Subsystem to use for the Prometheus metrics")
flag.Var(&customLabels, "label", "Additional labels to apply to the metrics")
flag.Parse()
// Check required options
if path == "" {
slog.Error("No path set")
os.Exit(1)
}
// Create a slice of all label keys to include user-defined additions
labelKeys := slices.Collect(maps.Keys(customLabels))
labelKeys = append(labelKeys, "extension")
// Instantiate Prometheus metrics
gauges = make(map[string]*prometheus.GaugeVec)
gauges["count"] = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "count",
Help: "Number of files found in the given directory",
},
labelKeys,
)
gauges["size"] = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "size",
Help: "Size (in bytes) of files found in the given directory",
},
labelKeys,
)
// Register Prometheus metrics
slog.Debug("Registering Prometheus metrics")
prometheus.MustRegister(
gauges["count"],
gauges["size"],
)
// Start the update process
extensionKeys = make(map[string]bool)
update()
go updateLoop()
// Bind HTTP handlers
slog.Debug("Binding HTTP handlers")
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "OK\n")
})
http.Handle("/metrics", promhttp.Handler())
// Run HTTP service
slog.Info("Serving HTTP requests", "address", httpHost, "port", httpPort)
err := http.ListenAndServe(httpHost+":"+strconv.Itoa(httpPort), nil)
slog.Error(err.Error())
}
func mergeLabels(extra map[string]string) prometheus.Labels {
labels := make(map[string]string)
maps.Copy(labels, customLabels)
maps.Copy(labels, extra)
return labels
}
func update() {
sizes := make(map[string]float64)
counts := make(map[string]float64)
slog.Debug("Scanning filesystem for updates")
err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
ext := filepath.Ext(info.Name())
// Remove the leading . if needed
if ext != "" {
ext = ext[1:]
}
extensionKeys[ext] = true
counts[ext]++
sizes[ext] += float64(info.Size())
return nil
})
if err != nil {
slog.Error(fmt.Sprintf("error walking the path %v\n", err))
return
}
// Loop over all keys and set the respective gauge values
slog.Debug("Writing new values to metrics")
for key, _ := range extensionKeys {
if _, ok := counts[key]; ok {
gauges["count"].With(mergeLabels(map[string]string{
"extension": key,
})).Set(float64(counts[key]))
gauges["size"].With(mergeLabels(map[string]string{
"extension": key,
})).Set(float64(sizes[key]))
continue
}
// Clean up extension which no longer exists in the directory
gauges["count"].Delete(mergeLabels(map[string]string{"extension": key}))
gauges["size"].Delete(mergeLabels(map[string]string{"extension": key}))
delete(extensionKeys, key)
}
}
func updateLoop() {
for range time.Tick(time.Duration(interval) * time.Second) {
update()
}
}
|