-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Expose certificate metrics (#16)
- Loading branch information
Showing
5 changed files
with
437 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package server | ||
|
||
import ( | ||
"log" | ||
"net/http" | ||
|
||
"github.com/canonical/gocert/internal/metrics" | ||
"github.com/prometheus/client_golang/prometheus/promhttp" | ||
) | ||
|
||
type middleware func(http.Handler) http.Handler | ||
|
||
// The middlewareContext type helps middleware receive and pass along information through the middleware chain. | ||
type middlewareContext struct { | ||
responseStatusCode int | ||
metrics *metrics.PrometheusMetrics | ||
} | ||
|
||
// The responseWriterCloner struct wraps the http.ResponseWriter struct, and extracts the status | ||
// code of the response writer for the middleware to read | ||
type responseWriterCloner struct { | ||
http.ResponseWriter | ||
statusCode int | ||
} | ||
|
||
// newResponseWriter returns a new ResponseWriterCloner struct | ||
// it returns http.StatusOK by default because the http.ResponseWriter defaults to that header | ||
// if the WriteHeader() function is never called. | ||
func newResponseWriter(w http.ResponseWriter) *responseWriterCloner { | ||
return &responseWriterCloner{w, http.StatusOK} | ||
} | ||
|
||
// WriteHeader overrides the ResponseWriter method to duplicate the status code into the wrapper struct | ||
func (rwc *responseWriterCloner) WriteHeader(code int) { | ||
rwc.statusCode = code | ||
rwc.ResponseWriter.WriteHeader(code) | ||
} | ||
|
||
// createMiddlewareStack chains the given middleware functions to wrap the api. | ||
// Each middleware functions calls next.ServeHTTP in order to resume the chain of execution. | ||
// The order the middleware functions are given to createMiddlewareStack matters. | ||
// Any code before next.ServeHTTP is called is executed in the given middleware's order. | ||
// Any code after next.ServeHTTP is called is executed in the given middleware's reverse order. | ||
func createMiddlewareStack(middleware ...middleware) middleware { | ||
return func(next http.Handler) http.Handler { | ||
for i := len(middleware) - 1; i >= 0; i-- { | ||
mw := middleware[i] | ||
next = mw(next) | ||
} | ||
return next | ||
} | ||
} | ||
|
||
// The Metrics middleware captures any request relevant to a metric and records it for prometheus. | ||
func metricsMiddleware(ctx *middlewareContext) middleware { | ||
return func(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
base := promhttp.InstrumentHandlerCounter( | ||
&ctx.metrics.RequestsTotal, | ||
promhttp.InstrumentHandlerDuration( | ||
&ctx.metrics.RequestsDuration, | ||
next, | ||
), | ||
) | ||
base.ServeHTTP(w, r) | ||
}) | ||
} | ||
} | ||
|
||
// The Logging middleware captures any http request coming through and the response status code, and logs it. | ||
func loggingMiddleware(ctx *middlewareContext) middleware { | ||
return func(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
clonedWwriter := newResponseWriter(w) | ||
next.ServeHTTP(w, r) | ||
log.Println(r.Method, r.URL.Path, clonedWwriter.statusCode, http.StatusText(clonedWwriter.statusCode)) | ||
ctx.responseStatusCode = clonedWwriter.statusCode | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,224 @@ | ||
package metrics | ||
|
||
import ( | ||
"crypto/x509" | ||
"encoding/pem" | ||
"errors" | ||
"log" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/canonical/gocert/internal/certdb" | ||
"github.com/prometheus/client_golang/prometheus" | ||
"github.com/prometheus/client_golang/prometheus/collectors" | ||
"github.com/prometheus/client_golang/prometheus/promhttp" | ||
) | ||
|
||
// Returns an HTTP handler for Prometheus metrics. | ||
func NewPrometheusMetricsHandler() http.Handler { | ||
reg := prometheus.NewRegistry() | ||
reg.MustRegister(collectors.NewGoCollector(), collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) | ||
prometheusHandler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{}) | ||
return prometheusHandler | ||
type PrometheusMetrics struct { | ||
http.Handler | ||
registry *prometheus.Registry | ||
CertificateRequests prometheus.Gauge | ||
OutstandingCertificateRequests prometheus.Gauge | ||
Certificates prometheus.Gauge | ||
CertificatesExpiringIn1Day prometheus.Gauge | ||
CertificatesExpiringIn7Days prometheus.Gauge | ||
CertificatesExpiringIn30Days prometheus.Gauge | ||
CertificatesExpiringIn90Days prometheus.Gauge | ||
ExpiredCertificates prometheus.Gauge | ||
|
||
RequestsTotal prometheus.CounterVec | ||
RequestsDuration prometheus.HistogramVec | ||
} | ||
|
||
// NewMetricsSubsystem returns the metrics endpoint HTTP handler and the Prometheus metrics collectors for the server and middleware. | ||
func NewMetricsSubsystem(db *certdb.CertificateRequestsRepository) *PrometheusMetrics { | ||
metricsBackend := newPrometheusMetrics() | ||
metricsBackend.Handler = promhttp.HandlerFor(metricsBackend.registry, promhttp.HandlerOpts{}) | ||
ticker := time.NewTicker(120 * time.Second) | ||
go func() { | ||
for ; ; <-ticker.C { | ||
csrs, err := db.RetrieveAll() | ||
if err != nil { | ||
log.Println(errors.Join(errors.New("error generating metrics repository: "), err)) | ||
panic(1) | ||
} | ||
metricsBackend.GenerateMetrics(csrs) | ||
} | ||
}() | ||
return metricsBackend | ||
} | ||
|
||
// newPrometheusMetrics reads the status of the database, calculates all of the values of the metrics, | ||
// registers these metrics to the prometheus registry, and returns the registry and the metrics. | ||
// The registry and metrics can be modified from this struct from anywhere in the codebase. | ||
func newPrometheusMetrics() *PrometheusMetrics { | ||
m := &PrometheusMetrics{ | ||
registry: prometheus.NewRegistry(), | ||
CertificateRequests: certificateRequestsMetric(), | ||
OutstandingCertificateRequests: outstandingCertificateRequestsMetric(), | ||
Certificates: certificatesMetric(), | ||
ExpiredCertificates: expiredCertificatesMetric(), | ||
CertificatesExpiringIn1Day: certificatesExpiringIn1DayMetric(), | ||
CertificatesExpiringIn7Days: certificatesExpiringIn7DaysMetric(), | ||
CertificatesExpiringIn30Days: certificatesExpiringIn30DaysMetric(), | ||
CertificatesExpiringIn90Days: certificatesExpiringIn90DaysMetric(), | ||
|
||
RequestsTotal: requestsTotalMetric(), | ||
RequestsDuration: requestDurationMetric(), | ||
} | ||
m.registry.MustRegister(m.CertificateRequests) | ||
m.registry.MustRegister(m.OutstandingCertificateRequests) | ||
m.registry.MustRegister(m.Certificates) | ||
m.registry.MustRegister(m.ExpiredCertificates) | ||
m.registry.MustRegister(m.CertificatesExpiringIn1Day) | ||
m.registry.MustRegister(m.CertificatesExpiringIn7Days) | ||
m.registry.MustRegister(m.CertificatesExpiringIn30Days) | ||
m.registry.MustRegister(m.CertificatesExpiringIn90Days) | ||
|
||
m.registry.MustRegister(m.RequestsTotal) | ||
m.registry.MustRegister(m.RequestsDuration) | ||
|
||
m.registry.MustRegister(collectors.NewGoCollector()) | ||
m.registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) | ||
return m | ||
} | ||
|
||
// GenerateMetrics receives the live list of csrs to calculate the most recent values for the metrics | ||
// defined for prometheus | ||
func (pm *PrometheusMetrics) GenerateMetrics(csrs []certdb.CertificateRequest) { | ||
var csrCount float64 = float64(len(csrs)) | ||
var outstandingCSRCount float64 | ||
var certCount float64 | ||
var expiredCertCount float64 | ||
var expiringIn1DayCertCount float64 | ||
var expiringIn7DaysCertCount float64 | ||
var expiringIn30DaysCertCount float64 | ||
var expiringIn90DaysCertCount float64 | ||
for _, entry := range csrs { | ||
if entry.Certificate == "" { | ||
outstandingCSRCount += 1 | ||
continue | ||
} | ||
if entry.Certificate == "rejected" { | ||
continue | ||
} | ||
certCount += 1 | ||
expiryDate := certificateExpiryDate(entry.Certificate) | ||
daysRemaining := time.Until(expiryDate).Hours() / 24 | ||
if daysRemaining < 0 { | ||
expiredCertCount += 1 | ||
} else { | ||
if daysRemaining < 1 { | ||
expiringIn1DayCertCount += 1 | ||
} | ||
if daysRemaining < 7 { | ||
expiringIn7DaysCertCount += 1 | ||
} | ||
if daysRemaining < 30 { | ||
expiringIn30DaysCertCount += 1 | ||
} | ||
if daysRemaining < 90 { | ||
expiringIn90DaysCertCount += 1 | ||
} | ||
} | ||
} | ||
pm.CertificateRequests.Set(csrCount) | ||
pm.OutstandingCertificateRequests.Set(outstandingCSRCount) | ||
pm.Certificates.Set(certCount) | ||
pm.ExpiredCertificates.Set(expiredCertCount) | ||
pm.CertificatesExpiringIn1Day.Set(expiringIn1DayCertCount) | ||
pm.CertificatesExpiringIn7Days.Set(expiringIn7DaysCertCount) | ||
pm.CertificatesExpiringIn30Days.Set(expiringIn30DaysCertCount) | ||
pm.CertificatesExpiringIn90Days.Set(expiringIn90DaysCertCount) | ||
} | ||
|
||
func certificateRequestsMetric() prometheus.Gauge { | ||
metric := prometheus.NewGauge(prometheus.GaugeOpts{ | ||
Name: "certificate_requests", | ||
Help: "Total number of certificate requests", | ||
}) | ||
return metric | ||
} | ||
|
||
func outstandingCertificateRequestsMetric() prometheus.Gauge { | ||
metric := prometheus.NewGauge(prometheus.GaugeOpts{ | ||
Name: "outstanding_certificate_requests", | ||
Help: "Number of outstanding certificate requests", | ||
}) | ||
return metric | ||
} | ||
|
||
func certificatesMetric() prometheus.Gauge { | ||
metric := prometheus.NewGauge(prometheus.GaugeOpts{ | ||
Name: "certificates", | ||
Help: "Total number of certificates provided to certificate requests", | ||
}) | ||
return metric | ||
} | ||
|
||
func expiredCertificatesMetric() prometheus.Gauge { | ||
metric := prometheus.NewGauge(prometheus.GaugeOpts{ | ||
Name: "certificates_expired", | ||
Help: "Number of expired certificates", | ||
}) | ||
return metric | ||
} | ||
|
||
func certificatesExpiringIn1DayMetric() prometheus.Gauge { | ||
metric := prometheus.NewGauge(prometheus.GaugeOpts{ | ||
Name: "certificates_expiring_in_1_day", | ||
Help: "Number of certificates expiring in less than 1 day", | ||
}) | ||
return metric | ||
} | ||
|
||
func certificatesExpiringIn7DaysMetric() prometheus.Gauge { | ||
metric := prometheus.NewGauge(prometheus.GaugeOpts{ | ||
Name: "certificates_expiring_in_7_days", | ||
Help: "Number of certificates expiring in less than 7 days", | ||
}) | ||
return metric | ||
} | ||
func certificatesExpiringIn30DaysMetric() prometheus.Gauge { | ||
metric := prometheus.NewGauge(prometheus.GaugeOpts{ | ||
Name: "certificates_expiring_in_30_days", | ||
Help: "Number of certificates expiring in less than 30 days", | ||
}) | ||
return metric | ||
} | ||
|
||
func certificatesExpiringIn90DaysMetric() prometheus.Gauge { | ||
metric := prometheus.NewGauge(prometheus.GaugeOpts{ | ||
Name: "certificates_expiring_in_90_days", | ||
Help: "Number of certificates expiring in less than 90 days", | ||
}) | ||
return metric | ||
} | ||
|
||
func requestsTotalMetric() prometheus.CounterVec { | ||
metric := prometheus.NewCounterVec( | ||
prometheus.CounterOpts{ | ||
Name: "http_requests_total", | ||
Help: "Tracks the number of HTTP requests.", | ||
}, []string{"method", "code"}, | ||
) | ||
return *metric | ||
} | ||
|
||
func requestDurationMetric() prometheus.HistogramVec { | ||
metric := prometheus.NewHistogramVec( | ||
prometheus.HistogramOpts{ | ||
Name: "http_request_duration_seconds", | ||
Help: "Tracks the latencies for HTTP requests.", | ||
Buckets: prometheus.ExponentialBuckets(0.1, 1.5, 5), | ||
}, []string{"method", "code"}, | ||
) | ||
return *metric | ||
} | ||
|
||
func certificateExpiryDate(certString string) time.Time { | ||
certBlock, _ := pem.Decode([]byte(certString)) | ||
cert, _ := x509.ParseCertificate(certBlock.Bytes) | ||
// TODO: cert.NotAfter can exist in a wrong cert. We should catch that at the db level validation | ||
return cert.NotAfter | ||
} |
Oops, something went wrong.