Add TLS certs expiration metric

This commit is contained in:
Sylvain Rabot 2020-12-18 18:44:03 +01:00 committed by GitHub
parent 3140a4e0cd
commit a3327c4430
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 291 additions and 62 deletions

View file

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"crypto/x509"
"encoding/json" "encoding/json"
stdlog "log" stdlog "log"
"net/http" "net/http"
@ -14,6 +15,7 @@ import (
"github.com/coreos/go-systemd/daemon" "github.com/coreos/go-systemd/daemon"
assetfs "github.com/elazarl/go-bindata-assetfs" assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge"
gokitmetrics "github.com/go-kit/kit/metrics"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/traefik/paerser/cli" "github.com/traefik/paerser/cli"
"github.com/traefik/traefik/v2/autogen/genstatic" "github.com/traefik/traefik/v2/autogen/genstatic"
@ -260,6 +262,11 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
watcher.AddListener(func(conf dynamic.Configuration) { watcher.AddListener(func(conf dynamic.Configuration) {
ctx := context.Background() ctx := context.Background()
tlsManager.UpdateConfigs(ctx, conf.TLS.Stores, conf.TLS.Options, conf.TLS.Certificates) tlsManager.UpdateConfigs(ctx, conf.TLS.Stores, conf.TLS.Options, conf.TLS.Certificates)
gauge := metricsRegistry.TLSCertsNotAfterTimestampGauge()
for _, certificate := range tlsManager.GetCertificates() {
appendCertMetric(gauge, certificate)
}
}) })
// Metrics // Metrics
@ -432,6 +439,20 @@ func registerMetricClients(metricsConfig *types.Metrics) []metrics.Registry {
return registries return registries
} }
func appendCertMetric(gauge gokitmetrics.Gauge, certificate *x509.Certificate) {
sort.Strings(certificate.DNSNames)
labels := []string{
"cn", certificate.Subject.CommonName,
"serial", certificate.SerialNumber.String(),
"sans", strings.Join(certificate.DNSNames, ","),
}
notAfter := float64(certificate.NotAfter.Unix())
gauge.With(labels...).Set(notAfter)
}
func setupAccessLog(conf *types.AccessLog) *accesslog.Handler { func setupAccessLog(conf *types.AccessLog) *accesslog.Handler {
if conf == nil { if conf == nil {
return nil return nil

116
cmd/traefik/traefik_test.go Normal file
View file

@ -0,0 +1,116 @@
package main
import (
"crypto/x509"
"encoding/pem"
"strings"
"testing"
"github.com/go-kit/kit/metrics"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// FooCert is a PEM-encoded TLS cert.
// generated from src/crypto/tls:
// go run generate_cert.go --rsa-bits 1024 --host foo.org,foo.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
const fooCert = `-----BEGIN CERTIFICATE-----
MIICHzCCAYigAwIBAgIQXQFLeYRwc5X21t457t2xADANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
iQKBgQDCjn67GSs/khuGC4GNN+tVo1S+/eSHwr/hWzhfMqO7nYiXkFzmxi+u14CU
Pda6WOeps7T2/oQEFMxKKg7zYOqkLSbjbE0ZfosopaTvEsZm/AZHAAvoOrAsIJOn
SEiwy8h0tLA4z1SNR6rmIVQWyqBZEPAhBTQM1z7tFp48FakCFwIDAQABo3QwcjAO
BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw
AwEB/zAdBgNVHQ4EFgQUDHG3ASzeUezElup9zbPpBn/vjogwGwYDVR0RBBQwEoIH
Zm9vLm9yZ4IHZm9vLmNvbTANBgkqhkiG9w0BAQsFAAOBgQBT+VLMbB9u27tBX8Aw
ZrGY3rbNdBGhXVTksrjiF+6ZtDpD3iI56GH9zLxnqvXkgn3u0+Ard5TqF/xmdwVw
NY0V/aWYfcL2G2auBCQrPvM03ozRnVUwVfP23eUzX2ORNHCYhd2ObQx4krrhs7cJ
SWxtKwFlstoXY3K2g9oRD9UxdQ==
-----END CERTIFICATE-----`
// BarCert is a PEM-encoded TLS cert.
// generated from src/crypto/tls:
// go run generate_cert.go --rsa-bits 1024 --host bar.org,bar.com --ca --start-date "Jan 1 00:00:00 1970" --duration=10000h
const barCert = `-----BEGIN CERTIFICATE-----
MIICHTCCAYagAwIBAgIQcuIcNEXzBHPoxna5S6wG4jANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMB4XDTcwMDEwMTAwMDAwMFoXDTcxMDIyMTE2MDAw
MFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC
gYEAqtcrP+KA7D6NjyztGNIPMup9KiBMJ8QL+preog/YHR7SQLO3kGFhpS3WKMab
SzMypC3ZX1PZjBP5ZzwaV3PFbuwlCkPlyxR2lOWmullgI7mjY0TBeYLDIclIzGRp
mpSDDSpkW1ay2iJDSpXjlhmwZr84hrCU7BRTQJo91fdsRTsCAwEAAaN0MHIwDgYD
VR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQFMAMB
Af8wHQYDVR0OBBYEFK8jnzFQvBAgWtfzOyXY4VSkwrTXMBsGA1UdEQQUMBKCB2Jh
ci5vcmeCB2Jhci5jb20wDQYJKoZIhvcNAQELBQADgYEAJz0ifAExisC/ZSRhWuHz
7qs1i6Nd4+YgEVR8dR71MChP+AMxucY1/ajVjb9xlLys3GPE90TWSdVppabEVjZY
Oq11nPKc50ItTt8dMku6t0JHBmzoGdkN0V4zJCBqdQJxhop8JpYJ0S9CW0eT93h3
ipYQSsmIINGtMXJ8VkP/MlM=
-----END CERTIFICATE-----`
type gaugeMock struct {
metrics map[string]float64
labels string
}
func (g gaugeMock) With(labelValues ...string) metrics.Gauge {
g.labels = strings.Join(labelValues, ",")
return g
}
func (g gaugeMock) Set(value float64) {
g.metrics[g.labels] = value
}
func (g gaugeMock) Add(delta float64) {
panic("implement me")
}
func TestAppendCertMetric(t *testing.T) {
testCases := []struct {
desc string
certs []string
expected map[string]float64
}{
{
desc: "No certs",
certs: []string{},
expected: map[string]float64{},
},
{
desc: "One cert",
certs: []string{fooCert},
expected: map[string]float64{
"cn,,serial,123624926713171615935660664614975025408,sans,foo.com,foo.org": 3.6e+09,
},
},
{
desc: "Two certs",
certs: []string{fooCert, barCert},
expected: map[string]float64{
"cn,,serial,123624926713171615935660664614975025408,sans,foo.com,foo.org": 3.6e+09,
"cn,,serial,152706022658490889223053211416725817058,sans,bar.com,bar.org": 3.6e+07,
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
gauge := &gaugeMock{
metrics: map[string]float64{},
}
for _, cert := range test.certs {
block, _ := pem.Decode([]byte(cert))
parsedCert, err := x509.ParseCertificate(block.Bytes)
require.NoError(t, err)
appendCertMetric(gauge, parsedCert)
}
assert.Equal(t, test.expected, gauge.metrics)
})
}
}

View file

@ -98,8 +98,8 @@ See the list of labels in the dedicated [routing](../routing/providers/docker.md
By default, Traefik watches for [container level labels](https://docs.docker.com/config/labels-custom-metadata/) on a standalone Docker Engine. By default, Traefik watches for [container level labels](https://docs.docker.com/config/labels-custom-metadata/) on a standalone Docker Engine.
When using Docker Compose, labels are specified by the directive When using Docker Compose, labels are specified by the directive
[`labels`](https://docs.docker.com/compose/compose-file/#labels) from the [`labels`](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels) from the
["services" objects](https://docs.docker.com/compose/compose-file/#service-configuration-reference). ["services" objects](https://docs.docker.com/compose/compose-file/compose-file-v3/#service-configuration-reference).
!!! tip "Not Only Docker" !!! tip "Not Only Docker"
Please note that any tool like Nomad, Terraform, Ansible, etc. Please note that any tool like Nomad, Terraform, Ansible, etc.
@ -186,7 +186,7 @@ set the [`swarmMode`](#swarmmode) directive to `true`.
While in Swarm Mode, Traefik uses labels found on services, not on individual containers. While in Swarm Mode, Traefik uses labels found on services, not on individual containers.
Therefore, if you use a compose file with Swarm Mode, labels should be defined in the Therefore, if you use a compose file with Swarm Mode, labels should be defined in the
[`deploy`](https://docs.docker.com/compose/compose-file/#labels-1) part of your service. [`deploy`](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels-1) part of your service.
This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file)). This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file)).

View file

@ -124,7 +124,7 @@ Attach labels to your containers and let Traefik do the rest!
!!! important "Labels in Docker Swarm Mode" !!! important "Labels in Docker Swarm Mode"
While in Swarm Mode, Traefik uses labels found on services, not on individual containers. While in Swarm Mode, Traefik uses labels found on services, not on individual containers.
Therefore, if you use a compose file with Swarm Mode, labels should be defined in the `deploy` part of your service. Therefore, if you use a compose file with Swarm Mode, labels should be defined in the `deploy` part of your service.
This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file/#labels-1)). This behavior is only enabled for docker-compose version 3+ ([Compose file reference](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels-1)).
## Routing Configuration ## Routing Configuration

View file

@ -20,18 +20,19 @@ var datadogTicker *time.Ticker
// Metric names consistent with https://github.com/DataDog/integrations-extras/pull/64 // Metric names consistent with https://github.com/DataDog/integrations-extras/pull/64
const ( const (
ddMetricsServiceReqsName = "service.request.total" ddMetricsServiceReqsName = "service.request.total"
ddMetricsServiceLatencyName = "service.request.duration" ddMetricsServiceLatencyName = "service.request.duration"
ddRetriesTotalName = "service.retries.total" ddRetriesTotalName = "service.retries.total"
ddConfigReloadsName = "config.reload.total" ddConfigReloadsName = "config.reload.total"
ddConfigReloadsFailureTagName = "failure" ddConfigReloadsFailureTagName = "failure"
ddLastConfigReloadSuccessName = "config.reload.lastSuccessTimestamp" ddLastConfigReloadSuccessName = "config.reload.lastSuccessTimestamp"
ddLastConfigReloadFailureName = "config.reload.lastFailureTimestamp" ddLastConfigReloadFailureName = "config.reload.lastFailureTimestamp"
ddEntryPointReqsName = "entrypoint.request.total" ddEntryPointReqsName = "entrypoint.request.total"
ddEntryPointReqDurationName = "entrypoint.request.duration" ddEntryPointReqDurationName = "entrypoint.request.duration"
ddEntryPointOpenConnsName = "entrypoint.connections.open" ddEntryPointOpenConnsName = "entrypoint.connections.open"
ddOpenConnsName = "service.connections.open" ddOpenConnsName = "service.connections.open"
ddServerUpName = "service.server.up" ddServerUpName = "service.server.up"
ddTLSCertsNotAfterTimestampName = "tls.certs.notAfterTimestamp"
) )
// RegisterDatadog registers the metrics pusher if this didn't happen yet and creates a datadog Registry instance. // RegisterDatadog registers the metrics pusher if this didn't happen yet and creates a datadog Registry instance.
@ -41,10 +42,11 @@ func RegisterDatadog(ctx context.Context, config *types.Datadog) Registry {
} }
registry := &standardRegistry{ registry := &standardRegistry{
configReloadsCounter: datadogClient.NewCounter(ddConfigReloadsName, 1.0), configReloadsCounter: datadogClient.NewCounter(ddConfigReloadsName, 1.0),
configReloadsFailureCounter: datadogClient.NewCounter(ddConfigReloadsName, 1.0).With(ddConfigReloadsFailureTagName, "true"), configReloadsFailureCounter: datadogClient.NewCounter(ddConfigReloadsName, 1.0).With(ddConfigReloadsFailureTagName, "true"),
lastConfigReloadSuccessGauge: datadogClient.NewGauge(ddLastConfigReloadSuccessName), lastConfigReloadSuccessGauge: datadogClient.NewGauge(ddLastConfigReloadSuccessName),
lastConfigReloadFailureGauge: datadogClient.NewGauge(ddLastConfigReloadFailureName), lastConfigReloadFailureGauge: datadogClient.NewGauge(ddLastConfigReloadFailureName),
tlsCertsNotAfterTimestampGauge: datadogClient.NewGauge(ddTLSCertsNotAfterTimestampName),
} }
if config.AddEntryPointsLabels { if config.AddEntryPointsLabels {

View file

@ -36,6 +36,7 @@ func TestDatadog(t *testing.T) {
"traefik.entrypoint.request.duration:10000.000000|h|#entrypoint:test\n", "traefik.entrypoint.request.duration:10000.000000|h|#entrypoint:test\n",
"traefik.entrypoint.connections.open:1.000000|g|#entrypoint:test\n", "traefik.entrypoint.connections.open:1.000000|g|#entrypoint:test\n",
"traefik.service.server.up:1.000000|g|#service:test,url:http://127.0.0.1,one:two\n", "traefik.service.server.up:1.000000|g|#service:test,url:http://127.0.0.1,one:two\n",
"traefik.tls.certs.notAfterTimestamp:1.000000|g|#key:value\n",
} }
udp.ShouldReceiveAll(t, expected, func() { udp.ShouldReceiveAll(t, expected, func() {
@ -50,5 +51,6 @@ func TestDatadog(t *testing.T) {
datadogRegistry.EntryPointReqDurationHistogram().With("entrypoint", "test").Observe(10000) datadogRegistry.EntryPointReqDurationHistogram().With("entrypoint", "test").Observe(10000)
datadogRegistry.EntryPointOpenConnsGauge().With("entrypoint", "test").Set(1) datadogRegistry.EntryPointOpenConnsGauge().With("entrypoint", "test").Set(1)
datadogRegistry.ServiceServerUpGauge().With("service", "test", "url", "http://127.0.0.1", "one", "two").Set(1) datadogRegistry.ServiceServerUpGauge().With("service", "test", "url", "http://127.0.0.1", "one", "two").Set(1)
datadogRegistry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1)
}) })
} }

View file

@ -26,18 +26,19 @@ type influxDBWriter struct {
var influxDBTicker *time.Ticker var influxDBTicker *time.Ticker
const ( const (
influxDBMetricsServiceReqsName = "traefik.service.requests.total" influxDBMetricsServiceReqsName = "traefik.service.requests.total"
influxDBMetricsServiceLatencyName = "traefik.service.request.duration" influxDBMetricsServiceLatencyName = "traefik.service.request.duration"
influxDBRetriesTotalName = "traefik.service.retries.total" influxDBRetriesTotalName = "traefik.service.retries.total"
influxDBConfigReloadsName = "traefik.config.reload.total" influxDBConfigReloadsName = "traefik.config.reload.total"
influxDBConfigReloadsFailureName = influxDBConfigReloadsName + ".failure" influxDBConfigReloadsFailureName = influxDBConfigReloadsName + ".failure"
influxDBLastConfigReloadSuccessName = "traefik.config.reload.lastSuccessTimestamp" influxDBLastConfigReloadSuccessName = "traefik.config.reload.lastSuccessTimestamp"
influxDBLastConfigReloadFailureName = "traefik.config.reload.lastFailureTimestamp" influxDBLastConfigReloadFailureName = "traefik.config.reload.lastFailureTimestamp"
influxDBEntryPointReqsName = "traefik.entrypoint.requests.total" influxDBEntryPointReqsName = "traefik.entrypoint.requests.total"
influxDBEntryPointReqDurationName = "traefik.entrypoint.request.duration" influxDBEntryPointReqDurationName = "traefik.entrypoint.request.duration"
influxDBEntryPointOpenConnsName = "traefik.entrypoint.connections.open" influxDBEntryPointOpenConnsName = "traefik.entrypoint.connections.open"
influxDBOpenConnsName = "traefik.service.connections.open" influxDBOpenConnsName = "traefik.service.connections.open"
influxDBServerUpName = "traefik.service.server.up" influxDBServerUpName = "traefik.service.server.up"
influxDBTLSCertsNotAfterTimestampName = "traefik.tls.certs.notAfterTimestamp"
) )
const ( const (
@ -55,10 +56,11 @@ func RegisterInfluxDB(ctx context.Context, config *types.InfluxDB) Registry {
} }
registry := &standardRegistry{ registry := &standardRegistry{
configReloadsCounter: influxDBClient.NewCounter(influxDBConfigReloadsName), configReloadsCounter: influxDBClient.NewCounter(influxDBConfigReloadsName),
configReloadsFailureCounter: influxDBClient.NewCounter(influxDBConfigReloadsFailureName), configReloadsFailureCounter: influxDBClient.NewCounter(influxDBConfigReloadsFailureName),
lastConfigReloadSuccessGauge: influxDBClient.NewGauge(influxDBLastConfigReloadSuccessName), lastConfigReloadSuccessGauge: influxDBClient.NewGauge(influxDBLastConfigReloadSuccessName),
lastConfigReloadFailureGauge: influxDBClient.NewGauge(influxDBLastConfigReloadFailureName), lastConfigReloadFailureGauge: influxDBClient.NewGauge(influxDBLastConfigReloadFailureName),
tlsCertsNotAfterTimestampGauge: influxDBClient.NewGauge(influxDBTLSCertsNotAfterTimestampName),
} }
if config.AddEntryPointsLabels { if config.AddEntryPointsLabels {

View file

@ -64,6 +64,16 @@ func TestInfluxDB(t *testing.T) {
}) })
assertMessage(t, msgEntrypoint, expectedEntrypoint) assertMessage(t, msgEntrypoint, expectedEntrypoint)
expectedTLS := []string{
`(traefik\.tls\.certs\.notAfterTimestamp,key=value value=1) [\d]{19}`,
}
msgTLS := udp.ReceiveString(t, func() {
influxDBRegistry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1)
})
assertMessage(t, msgTLS, expectedTLS)
} }
func TestInfluxDBHTTP(t *testing.T) { func TestInfluxDBHTTP(t *testing.T) {
@ -121,6 +131,15 @@ func TestInfluxDBHTTP(t *testing.T) {
msgEntrypoint := <-c msgEntrypoint := <-c
assertMessage(t, *msgEntrypoint, expectedEntrypoint) assertMessage(t, *msgEntrypoint, expectedEntrypoint)
expectedTLS := []string{
`(traefik\.tls\.certs\.notAfterTimestamp,key=value value=1) [\d]{19}`,
}
influxDBRegistry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1)
msgTLS := <-c
assertMessage(t, *msgTLS, expectedTLS)
} }
func assertMessage(t *testing.T, msg string, patterns []string) { func assertMessage(t *testing.T, msg string, patterns []string) {

View file

@ -21,6 +21,9 @@ type Registry interface {
LastConfigReloadSuccessGauge() metrics.Gauge LastConfigReloadSuccessGauge() metrics.Gauge
LastConfigReloadFailureGauge() metrics.Gauge LastConfigReloadFailureGauge() metrics.Gauge
// TLS
TLSCertsNotAfterTimestampGauge() metrics.Gauge
// entry point metrics // entry point metrics
EntryPointReqsCounter() metrics.Counter EntryPointReqsCounter() metrics.Counter
EntryPointReqsTLSCounter() metrics.Counter EntryPointReqsTLSCounter() metrics.Counter
@ -50,6 +53,7 @@ func NewMultiRegistry(registries []Registry) Registry {
var configReloadsFailureCounter []metrics.Counter var configReloadsFailureCounter []metrics.Counter
var lastConfigReloadSuccessGauge []metrics.Gauge var lastConfigReloadSuccessGauge []metrics.Gauge
var lastConfigReloadFailureGauge []metrics.Gauge var lastConfigReloadFailureGauge []metrics.Gauge
var tlsCertsNotAfterTimestampGauge []metrics.Gauge
var entryPointReqsCounter []metrics.Counter var entryPointReqsCounter []metrics.Counter
var entryPointReqsTLSCounter []metrics.Counter var entryPointReqsTLSCounter []metrics.Counter
var entryPointReqDurationHistogram []ScalableHistogram var entryPointReqDurationHistogram []ScalableHistogram
@ -74,6 +78,9 @@ func NewMultiRegistry(registries []Registry) Registry {
if r.LastConfigReloadFailureGauge() != nil { if r.LastConfigReloadFailureGauge() != nil {
lastConfigReloadFailureGauge = append(lastConfigReloadFailureGauge, r.LastConfigReloadFailureGauge()) lastConfigReloadFailureGauge = append(lastConfigReloadFailureGauge, r.LastConfigReloadFailureGauge())
} }
if r.TLSCertsNotAfterTimestampGauge() != nil {
tlsCertsNotAfterTimestampGauge = append(tlsCertsNotAfterTimestampGauge, r.TLSCertsNotAfterTimestampGauge())
}
if r.EntryPointReqsCounter() != nil { if r.EntryPointReqsCounter() != nil {
entryPointReqsCounter = append(entryPointReqsCounter, r.EntryPointReqsCounter()) entryPointReqsCounter = append(entryPointReqsCounter, r.EntryPointReqsCounter())
} }
@ -113,6 +120,7 @@ func NewMultiRegistry(registries []Registry) Registry {
configReloadsFailureCounter: multi.NewCounter(configReloadsFailureCounter...), configReloadsFailureCounter: multi.NewCounter(configReloadsFailureCounter...),
lastConfigReloadSuccessGauge: multi.NewGauge(lastConfigReloadSuccessGauge...), lastConfigReloadSuccessGauge: multi.NewGauge(lastConfigReloadSuccessGauge...),
lastConfigReloadFailureGauge: multi.NewGauge(lastConfigReloadFailureGauge...), lastConfigReloadFailureGauge: multi.NewGauge(lastConfigReloadFailureGauge...),
tlsCertsNotAfterTimestampGauge: multi.NewGauge(tlsCertsNotAfterTimestampGauge...),
entryPointReqsCounter: multi.NewCounter(entryPointReqsCounter...), entryPointReqsCounter: multi.NewCounter(entryPointReqsCounter...),
entryPointReqsTLSCounter: multi.NewCounter(entryPointReqsTLSCounter...), entryPointReqsTLSCounter: multi.NewCounter(entryPointReqsTLSCounter...),
entryPointReqDurationHistogram: NewMultiHistogram(entryPointReqDurationHistogram...), entryPointReqDurationHistogram: NewMultiHistogram(entryPointReqDurationHistogram...),
@ -133,6 +141,7 @@ type standardRegistry struct {
configReloadsFailureCounter metrics.Counter configReloadsFailureCounter metrics.Counter
lastConfigReloadSuccessGauge metrics.Gauge lastConfigReloadSuccessGauge metrics.Gauge
lastConfigReloadFailureGauge metrics.Gauge lastConfigReloadFailureGauge metrics.Gauge
tlsCertsNotAfterTimestampGauge metrics.Gauge
entryPointReqsCounter metrics.Counter entryPointReqsCounter metrics.Counter
entryPointReqsTLSCounter metrics.Counter entryPointReqsTLSCounter metrics.Counter
entryPointReqDurationHistogram ScalableHistogram entryPointReqDurationHistogram ScalableHistogram
@ -169,6 +178,10 @@ func (r *standardRegistry) LastConfigReloadFailureGauge() metrics.Gauge {
return r.lastConfigReloadFailureGauge return r.lastConfigReloadFailureGauge
} }
func (r *standardRegistry) TLSCertsNotAfterTimestampGauge() metrics.Gauge {
return r.tlsCertsNotAfterTimestampGauge
}
func (r *standardRegistry) EntryPointReqsCounter() metrics.Counter { func (r *standardRegistry) EntryPointReqsCounter() metrics.Counter {
return r.entryPointReqsCounter return r.entryPointReqsCounter
} }

View file

@ -29,6 +29,10 @@ const (
configLastReloadSuccessName = metricConfigPrefix + "last_reload_success" configLastReloadSuccessName = metricConfigPrefix + "last_reload_success"
configLastReloadFailureName = metricConfigPrefix + "last_reload_failure" configLastReloadFailureName = metricConfigPrefix + "last_reload_failure"
// TLS.
metricsTLSPrefix = MetricNamePrefix + "tls_"
tlsCertsNotAfterTimestamp = metricsTLSPrefix + "certs_not_after"
// entry point. // entry point.
metricEntryPointPrefix = MetricNamePrefix + "entrypoint_" metricEntryPointPrefix = MetricNamePrefix + "entrypoint_"
entryPointReqsTotalName = metricEntryPointPrefix + "requests_total" entryPointReqsTotalName = metricEntryPointPrefix + "requests_total"
@ -121,21 +125,27 @@ func initStandardRegistry(config *types.Prometheus) Registry {
Name: configLastReloadFailureName, Name: configLastReloadFailureName,
Help: "Last config reload failure", Help: "Last config reload failure",
}, []string{}) }, []string{})
tlsCertsNotAfterTimesptamp := newGaugeFrom(promState.collectors, stdprometheus.GaugeOpts{
Name: tlsCertsNotAfterTimestamp,
Help: "Certificate expiration timestamp",
}, []string{"cn", "serial", "sans"})
promState.describers = []func(chan<- *stdprometheus.Desc){ promState.describers = []func(chan<- *stdprometheus.Desc){
configReloads.cv.Describe, configReloads.cv.Describe,
configReloadsFailures.cv.Describe, configReloadsFailures.cv.Describe,
lastConfigReloadSuccess.gv.Describe, lastConfigReloadSuccess.gv.Describe,
lastConfigReloadFailure.gv.Describe, lastConfigReloadFailure.gv.Describe,
tlsCertsNotAfterTimesptamp.gv.Describe,
} }
reg := &standardRegistry{ reg := &standardRegistry{
epEnabled: config.AddEntryPointsLabels, epEnabled: config.AddEntryPointsLabels,
svcEnabled: config.AddServicesLabels, svcEnabled: config.AddServicesLabels,
configReloadsCounter: configReloads, configReloadsCounter: configReloads,
configReloadsFailureCounter: configReloadsFailures, configReloadsFailureCounter: configReloadsFailures,
lastConfigReloadSuccessGauge: lastConfigReloadSuccess, lastConfigReloadSuccessGauge: lastConfigReloadSuccess,
lastConfigReloadFailureGauge: lastConfigReloadFailure, lastConfigReloadFailureGauge: lastConfigReloadFailure,
tlsCertsNotAfterTimestampGauge: tlsCertsNotAfterTimesptamp,
} }
if config.AddEntryPointsLabels { if config.AddEntryPointsLabels {
@ -163,11 +173,13 @@ func initStandardRegistry(config *types.Prometheus) Registry {
entryPointReqDurations.hv.Describe, entryPointReqDurations.hv.Describe,
entryPointOpenConns.gv.Describe, entryPointOpenConns.gv.Describe,
}...) }...)
reg.entryPointReqsCounter = entryPointReqs reg.entryPointReqsCounter = entryPointReqs
reg.entryPointReqsTLSCounter = entryPointReqsTLS reg.entryPointReqsTLSCounter = entryPointReqsTLS
reg.entryPointReqDurationHistogram, _ = NewHistogramWithScale(entryPointReqDurations, time.Second) reg.entryPointReqDurationHistogram, _ = NewHistogramWithScale(entryPointReqDurations, time.Second)
reg.entryPointOpenConnsGauge = entryPointOpenConns reg.entryPointOpenConnsGauge = entryPointOpenConns
} }
if config.AddServicesLabels { if config.AddServicesLabels {
serviceReqs := newCounterFrom(promState.collectors, stdprometheus.CounterOpts{ serviceReqs := newCounterFrom(promState.collectors, stdprometheus.CounterOpts{
Name: serviceReqsTotalName, Name: serviceReqsTotalName,

View file

@ -116,6 +116,11 @@ func TestPrometheus(t *testing.T) {
prometheusRegistry.LastConfigReloadSuccessGauge().Set(float64(time.Now().Unix())) prometheusRegistry.LastConfigReloadSuccessGauge().Set(float64(time.Now().Unix()))
prometheusRegistry.LastConfigReloadFailureGauge().Set(float64(time.Now().Unix())) prometheusRegistry.LastConfigReloadFailureGauge().Set(float64(time.Now().Unix()))
prometheusRegistry.
TLSCertsNotAfterTimestampGauge().
With("cn", "value", "serial", "value", "sans", "value").
Set(float64(time.Now().Unix()))
prometheusRegistry. prometheusRegistry.
EntryPointReqsCounter(). EntryPointReqsCounter().
With("code", strconv.Itoa(http.StatusOK), "method", http.MethodGet, "protocol", "http", "entrypoint", "http"). With("code", strconv.Itoa(http.StatusOK), "method", http.MethodGet, "protocol", "http", "entrypoint", "http").
@ -175,6 +180,15 @@ func TestPrometheus(t *testing.T) {
name: configLastReloadFailureName, name: configLastReloadFailureName,
assert: buildTimestampAssert(t, configLastReloadFailureName), assert: buildTimestampAssert(t, configLastReloadFailureName),
}, },
{
name: tlsCertsNotAfterTimestamp,
labels: map[string]string{
"cn": "value",
"serial": "value",
"sans": "value",
},
assert: buildTimestampAssert(t, tlsCertsNotAfterTimestamp),
},
{ {
name: entryPointReqsTotalName, name: entryPointReqsTotalName,
labels: map[string]string{ labels: map[string]string{

View file

@ -17,18 +17,19 @@ var (
) )
const ( const (
statsdMetricsServiceReqsName = "service.request.total" statsdMetricsServiceReqsName = "service.request.total"
statsdMetricsServiceLatencyName = "service.request.duration" statsdMetricsServiceLatencyName = "service.request.duration"
statsdRetriesTotalName = "service.retries.total" statsdRetriesTotalName = "service.retries.total"
statsdConfigReloadsName = "config.reload.total" statsdConfigReloadsName = "config.reload.total"
statsdConfigReloadsFailureName = statsdConfigReloadsName + ".failure" statsdConfigReloadsFailureName = statsdConfigReloadsName + ".failure"
statsdLastConfigReloadSuccessName = "config.reload.lastSuccessTimestamp" statsdLastConfigReloadSuccessName = "config.reload.lastSuccessTimestamp"
statsdLastConfigReloadFailureName = "config.reload.lastFailureTimestamp" statsdLastConfigReloadFailureName = "config.reload.lastFailureTimestamp"
statsdEntryPointReqsName = "entrypoint.request.total" statsdEntryPointReqsName = "entrypoint.request.total"
statsdEntryPointReqDurationName = "entrypoint.request.duration" statsdEntryPointReqDurationName = "entrypoint.request.duration"
statsdEntryPointOpenConnsName = "entrypoint.connections.open" statsdEntryPointOpenConnsName = "entrypoint.connections.open"
statsdOpenConnsName = "service.connections.open" statsdOpenConnsName = "service.connections.open"
statsdServerUpName = "service.server.up" statsdServerUpName = "service.server.up"
statsdTLSCertsNotAfterTimestampName = "tls.certs.notAfterTimestamp"
) )
// RegisterStatsd registers the metrics pusher if this didn't happen yet and creates a statsd Registry instance. // RegisterStatsd registers the metrics pusher if this didn't happen yet and creates a statsd Registry instance.
@ -48,10 +49,11 @@ func RegisterStatsd(ctx context.Context, config *types.Statsd) Registry {
} }
registry := &standardRegistry{ registry := &standardRegistry{
configReloadsCounter: statsdClient.NewCounter(statsdConfigReloadsName, 1.0), configReloadsCounter: statsdClient.NewCounter(statsdConfigReloadsName, 1.0),
configReloadsFailureCounter: statsdClient.NewCounter(statsdConfigReloadsFailureName, 1.0), configReloadsFailureCounter: statsdClient.NewCounter(statsdConfigReloadsFailureName, 1.0),
lastConfigReloadSuccessGauge: statsdClient.NewGauge(statsdLastConfigReloadSuccessName), lastConfigReloadSuccessGauge: statsdClient.NewGauge(statsdLastConfigReloadSuccessName),
lastConfigReloadFailureGauge: statsdClient.NewGauge(statsdLastConfigReloadFailureName), lastConfigReloadFailureGauge: statsdClient.NewGauge(statsdLastConfigReloadFailureName),
tlsCertsNotAfterTimestampGauge: statsdClient.NewGauge(statsdTLSCertsNotAfterTimestampName),
} }
if config.AddEntryPointsLabels { if config.AddEntryPointsLabels {

View file

@ -35,6 +35,7 @@ func TestStatsD(t *testing.T) {
"traefik.entrypoint.request.duration:10000.000000|ms", "traefik.entrypoint.request.duration:10000.000000|ms",
"traefik.entrypoint.connections.open:1.000000|g\n", "traefik.entrypoint.connections.open:1.000000|g\n",
"traefik.service.server.up:1.000000|g\n", "traefik.service.server.up:1.000000|g\n",
"tls.certs.notAfterTimestamp:1.000000|g\n",
} }
udp.ShouldReceiveAll(t, expected, func() { udp.ShouldReceiveAll(t, expected, func() {
@ -49,6 +50,7 @@ func TestStatsD(t *testing.T) {
statsdRegistry.EntryPointReqDurationHistogram().With("entrypoint", "test").Observe(10000) statsdRegistry.EntryPointReqDurationHistogram().With("entrypoint", "test").Observe(10000)
statsdRegistry.EntryPointOpenConnsGauge().With("entrypoint", "test").Set(1) statsdRegistry.EntryPointOpenConnsGauge().With("entrypoint", "test").Set(1)
statsdRegistry.ServiceServerUpGauge().With("service:test", "url", "http://127.0.0.1").Set(1) statsdRegistry.ServiceServerUpGauge().With("service:test", "url", "http://127.0.0.1").Set(1)
statsdRegistry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1)
}) })
} }
@ -75,6 +77,7 @@ func TestStatsDWithPrefix(t *testing.T) {
"testPrefix.entrypoint.request.duration:10000.000000|ms", "testPrefix.entrypoint.request.duration:10000.000000|ms",
"testPrefix.entrypoint.connections.open:1.000000|g\n", "testPrefix.entrypoint.connections.open:1.000000|g\n",
"testPrefix.service.server.up:1.000000|g\n", "testPrefix.service.server.up:1.000000|g\n",
"tls.certs.notAfterTimestamp:1.000000|g\n",
} }
udp.ShouldReceiveAll(t, expected, func() { udp.ShouldReceiveAll(t, expected, func() {
@ -89,5 +92,6 @@ func TestStatsDWithPrefix(t *testing.T) {
statsdRegistry.EntryPointReqDurationHistogram().With("entrypoint", "test").Observe(10000) statsdRegistry.EntryPointReqDurationHistogram().With("entrypoint", "test").Observe(10000)
statsdRegistry.EntryPointOpenConnsGauge().With("entrypoint", "test").Set(1) statsdRegistry.EntryPointOpenConnsGauge().With("entrypoint", "test").Set(1)
statsdRegistry.ServiceServerUpGauge().With("service:test", "url", "http://127.0.0.1").Set(1) statsdRegistry.ServiceServerUpGauge().With("service:test", "url", "http://127.0.0.1").Set(1)
statsdRegistry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1)
}) })
} }

View file

@ -56,15 +56,16 @@ func (c CertificateStore) getDefaultCertificateDomains() []string {
// GetAllDomains return a slice with all the certificate domain. // GetAllDomains return a slice with all the certificate domain.
func (c CertificateStore) GetAllDomains() []string { func (c CertificateStore) GetAllDomains() []string {
allCerts := c.getDefaultCertificateDomains() allDomains := c.getDefaultCertificateDomains()
// Get dynamic certificates // Get dynamic certificates
if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil { if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil {
for domains := range c.DynamicCerts.Get().(map[string]*tls.Certificate) { for domain := range c.DynamicCerts.Get().(map[string]*tls.Certificate) {
allCerts = append(allCerts, domains) allDomains = append(allDomains, domain)
} }
} }
return allCerts
return allDomains
} }
// GetBestCertificate returns the best match certificate, and caches the response. // GetBestCertificate returns the best match certificate, and caches the response.

View file

@ -131,6 +131,27 @@ func (m *Manager) Get(storeName, configName string) (*tls.Config, error) {
return tlsConfig, err return tlsConfig, err
} }
// GetCertificates returns all stored certificates.
func (m *Manager) GetCertificates() []*x509.Certificate {
var certificates []*x509.Certificate
// We iterate over all the certificates.
for _, store := range m.stores {
if store.DynamicCerts != nil && store.DynamicCerts.Get() != nil {
for _, cert := range store.DynamicCerts.Get().(map[string]*tls.Certificate) {
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
continue
}
certificates = append(certificates, x509Cert)
}
}
}
return certificates
}
func (m *Manager) getStore(storeName string) *CertificateStore { func (m *Manager) getStore(storeName string) *CertificateStore {
_, ok := m.stores[storeName] _, ok := m.stores[storeName]
if !ok { if !ok {