Compare commits
6 Commits
0a3d0c0191
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a336225ac8 | |||
| 32517b4e41 | |||
| 5321a86b2c | |||
| 1fc12339ba | |||
| 4ceb2023db | |||
| d4f74661d5 |
10
.woodpecker.yml
Normal file
10
.woodpecker.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
steps:
|
||||||
|
build:
|
||||||
|
image: "golang:1.21.5-alpine"
|
||||||
|
commands:
|
||||||
|
- go build main.go
|
||||||
|
lint:
|
||||||
|
image: "golang:1.21.5-alpine"
|
||||||
|
commands:
|
||||||
|
- go fmt ./main.go
|
||||||
|
- go vet ./main.go
|
||||||
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM golang@sha256:fe5bea2e1ab3ffebe0267393fea88fcb197e2dbbb1e2dbabeec6dd9ccb0e1871 AS builder
|
||||||
|
|
||||||
|
COPY . /build
|
||||||
|
WORKDIR /build
|
||||||
|
RUN go build -o cert-exporter main.go
|
||||||
|
|
||||||
|
FROM alpine@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d
|
||||||
|
COPY --from=builder /build/cert-exporter /opt/cert-exporter
|
||||||
|
ENTRYPOINT ["/opt/cert-exporter"]
|
||||||
23
README.md
23
README.md
@@ -1,4 +1,23 @@
|
|||||||
# certs-status-exporter
|
# cert-status-exporter
|
||||||
|
|
||||||
A Prometheus exporter that checks the expiry and validity of configured domains
|
A Prometheus exporter that checks the expiry and validity of configured domains
|
||||||
once a day.
|
once a day.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To export certificate status metrics, run `cert-status-exporter --domain <domain>:<port>:<alpn>[:<name>]`.
|
||||||
|
The format means: "Connect to `<domain>` on port `<port>` while advertising `<alpn>` as ALPN during the
|
||||||
|
TLS negotiations. If `<name>` is specified, then the metric will be labeled as `<name>`, instead of `<domain>`.
|
||||||
|
`<alpn>` is a semi-colon separated list of ALPN protocols.
|
||||||
|
|
||||||
|
By default, `cert-status-exporter` will bind to `0.0.0.0:8888` and expose the metrics at `0.0.0.0:8888/metrics`. To change
|
||||||
|
the binding address, you can specify `--host` and `--port` to change how `cert-status-exporter` binds the socket.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
- `cert-status-exporter --host 127.0.0.1 --port 8383 --domain "gnu.org:443:http/1.1;http/1.0;http/0.9"`: Check the certificate of `gnu.org:443`, while presenting the HTTP ALPN protocol names during the TLS negotiation. The metrics are exported with the label `name` equal to `gnu.org`.
|
||||||
|
- `cert-status-exporter --domain "example.org:5223:xmpp-client:xmpp"`: Check the certificate of `example.org:5223`, while presenting `xmpp-client` as the ALPN protocol during the TLS negotiation. Moreover, export the certificate state with the label `name` equal to `xmpp`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See `LICENSE`.
|
||||||
38
main.go
38
main.go
@@ -28,7 +28,7 @@ type CertificateMetrics struct {
|
|||||||
IsValid prometheus.Gauge
|
IsValid prometheus.Gauge
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCertificateMetrics(domain string, port int, alpn []string) CertificateMetrics {
|
func NewCertificateMetrics(domain string, port int, alpn []string, name string) CertificateMetrics {
|
||||||
metrics := CertificateMetrics{
|
metrics := CertificateMetrics{
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Port: port,
|
Port: port,
|
||||||
@@ -36,13 +36,13 @@ func NewCertificateMetrics(domain string, port int, alpn []string) CertificateMe
|
|||||||
ExpiryIn: prometheus.NewGauge(
|
ExpiryIn: prometheus.NewGauge(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "cert_status_expiry_in",
|
Name: "cert_status_expiry_in",
|
||||||
ConstLabels: prometheus.Labels{"domain": domain},
|
ConstLabels: prometheus.Labels{"name": name},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IsValid: prometheus.NewGauge(
|
IsValid: prometheus.NewGauge(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "cert_status_is_valid",
|
Name: "cert_status_is_valid",
|
||||||
ConstLabels: prometheus.Labels{"domain": domain},
|
ConstLabels: prometheus.Labels{"name": name},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -88,8 +88,9 @@ func (c *CertificateMetrics) checkTls() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateMetrics(metrics *[]CertificateMetrics) {
|
func updateMetrics(metrics *[]CertificateMetrics, lastUpdated prometheus.Gauge) {
|
||||||
log.Debugf("Updating metrics for %d domains...", len(*metrics))
|
log.Debugf("Updating metrics for %d domains...", len(*metrics))
|
||||||
|
lastUpdated.SetToCurrentTime()
|
||||||
for _, metric := range *metrics {
|
for _, metric := range *metrics {
|
||||||
metric.checkTls()
|
metric.checkTls()
|
||||||
}
|
}
|
||||||
@@ -108,15 +109,29 @@ func run(ctx *cli.Context) error {
|
|||||||
|
|
||||||
// Setup metrics
|
// Setup metrics
|
||||||
registry := prometheus.NewRegistry()
|
registry := prometheus.NewRegistry()
|
||||||
|
lastUpdatedMetric := prometheus.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "certs_status_last_updated",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
registry.MustRegister(lastUpdatedMetric)
|
||||||
|
|
||||||
metrics := make([]CertificateMetrics, 0)
|
metrics := make([]CertificateMetrics, 0)
|
||||||
for _, d := range domains {
|
for _, d := range domains {
|
||||||
log.Debugf("Parsing '%s'...", d)
|
log.Debugf("Parsing '%s'...", d)
|
||||||
parts := strings.Split(d, ":")
|
parts := strings.Split(d, ":")
|
||||||
if len(parts) != 3 {
|
if len(parts) < 3 {
|
||||||
log.Errorf("Invalid domain format for '%s'", d)
|
log.Errorf("Invalid domain format for '%s'", d)
|
||||||
return errors.New("Invalid domain format: Expects <domain>:<port>:<alpn>")
|
return errors.New("Invalid domain format: Expects <domain>:<port>:<alpn>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
name := ""
|
||||||
|
if len(parts) == 4 {
|
||||||
|
name = parts[3]
|
||||||
|
} else {
|
||||||
|
name = parts[0]
|
||||||
|
}
|
||||||
|
|
||||||
port, err := strconv.Atoi(parts[1])
|
port, err := strconv.Atoi(parts[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to parse port of '%s'", d)
|
log.Errorf("Failed to parse port of '%s'", d)
|
||||||
@@ -126,11 +141,12 @@ func run(ctx *cli.Context) error {
|
|||||||
// Create the metric, and register it
|
// Create the metric, and register it
|
||||||
// TODO: Make this prettier
|
// TODO: Make this prettier
|
||||||
alpn := strings.Split(parts[2], ";")
|
alpn := strings.Split(parts[2], ";")
|
||||||
log.Debugf("Using ALPNs: %v", alpn)
|
log.Debugf("Registering: domain='%s' port='%d' alpn=%v name='%s'", parts[0], port, alpn, name)
|
||||||
metric := NewCertificateMetrics(
|
metric := NewCertificateMetrics(
|
||||||
parts[0],
|
parts[0],
|
||||||
port,
|
port,
|
||||||
alpn,
|
alpn,
|
||||||
|
name,
|
||||||
)
|
)
|
||||||
metric.register(registry)
|
metric.register(registry)
|
||||||
metrics = append(metrics, metric)
|
metrics = append(metrics, metric)
|
||||||
@@ -147,20 +163,20 @@ func run(ctx *cli.Context) error {
|
|||||||
gocron.DurationJob(24*time.Hour),
|
gocron.DurationJob(24*time.Hour),
|
||||||
gocron.NewTask(
|
gocron.NewTask(
|
||||||
func() {
|
func() {
|
||||||
updateMetrics(&metrics)
|
updateMetrics(&metrics, lastUpdatedMetric)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
gocron.WithStartAt(gocron.WithStartImmediately()),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to create periodic task")
|
log.Error("Failed to create periodic task")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform an initial run to populate the metrics
|
// Start the scheduler
|
||||||
log.Info("Performing initial requests...")
|
scheduler.Start()
|
||||||
updateMetrics(&metrics)
|
|
||||||
log.Debug("Done")
|
|
||||||
|
|
||||||
|
// Handle HTTP requests
|
||||||
http.Handle(
|
http.Handle(
|
||||||
"/metrics", promhttp.HandlerFor(
|
"/metrics", promhttp.HandlerFor(
|
||||||
registry,
|
registry,
|
||||||
|
|||||||
Reference in New Issue
Block a user