diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index d55eaa6a6..bfe335c7a 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/x509" "encoding/json" stdlog "log" "net/http" @@ -14,6 +15,7 @@ import ( "github.com/coreos/go-systemd/daemon" assetfs "github.com/elazarl/go-bindata-assetfs" "github.com/go-acme/lego/v4/challenge" + gokitmetrics "github.com/go-kit/kit/metrics" "github.com/sirupsen/logrus" "github.com/traefik/paerser/cli" "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) { ctx := context.Background() tlsManager.UpdateConfigs(ctx, conf.TLS.Stores, conf.TLS.Options, conf.TLS.Certificates) + + gauge := metricsRegistry.TLSCertsNotAfterTimestampGauge() + for _, certificate := range tlsManager.GetCertificates() { + appendCertMetric(gauge, certificate) + } }) // Metrics @@ -432,6 +439,20 @@ func registerMetricClients(metricsConfig *types.Metrics) []metrics.Registry { 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 { if conf == nil { return nil diff --git a/cmd/traefik/traefik_test.go b/cmd/traefik/traefik_test.go new file mode 100644 index 000000000..26bc8643c --- /dev/null +++ b/cmd/traefik/traefik_test.go @@ -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) + }) + } +} diff --git a/docs/content/providers/docker.md b/docs/content/providers/docker.md index 1a7fff417..439e53a0e 100644 --- a/docs/content/providers/docker.md +++ b/docs/content/providers/docker.md @@ -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. When using Docker Compose, labels are specified by the directive -[`labels`](https://docs.docker.com/compose/compose-file/#labels) from the -["services" objects](https://docs.docker.com/compose/compose-file/#service-configuration-reference). +[`labels`](https://docs.docker.com/compose/compose-file/compose-file-v3/#labels) from the +["services" objects](https://docs.docker.com/compose/compose-file/compose-file-v3/#service-configuration-reference). !!! tip "Not Only Docker" 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. 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)). diff --git a/docs/content/routing/providers/docker.md b/docs/content/routing/providers/docker.md index 2a58fa806..21f986a56 100644 --- a/docs/content/routing/providers/docker.md +++ b/docs/content/routing/providers/docker.md @@ -124,7 +124,7 @@ Attach labels to your containers and let Traefik do the rest! !!! important "Labels in Docker Swarm Mode" 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. - 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 diff --git a/pkg/metrics/datadog.go b/pkg/metrics/datadog.go index 605426689..e7f28d65d 100644 --- a/pkg/metrics/datadog.go +++ b/pkg/metrics/datadog.go @@ -20,18 +20,19 @@ var datadogTicker *time.Ticker // Metric names consistent with https://github.com/DataDog/integrations-extras/pull/64 const ( - ddMetricsServiceReqsName = "service.request.total" - ddMetricsServiceLatencyName = "service.request.duration" - ddRetriesTotalName = "service.retries.total" - ddConfigReloadsName = "config.reload.total" - ddConfigReloadsFailureTagName = "failure" - ddLastConfigReloadSuccessName = "config.reload.lastSuccessTimestamp" - ddLastConfigReloadFailureName = "config.reload.lastFailureTimestamp" - ddEntryPointReqsName = "entrypoint.request.total" - ddEntryPointReqDurationName = "entrypoint.request.duration" - ddEntryPointOpenConnsName = "entrypoint.connections.open" - ddOpenConnsName = "service.connections.open" - ddServerUpName = "service.server.up" + ddMetricsServiceReqsName = "service.request.total" + ddMetricsServiceLatencyName = "service.request.duration" + ddRetriesTotalName = "service.retries.total" + ddConfigReloadsName = "config.reload.total" + ddConfigReloadsFailureTagName = "failure" + ddLastConfigReloadSuccessName = "config.reload.lastSuccessTimestamp" + ddLastConfigReloadFailureName = "config.reload.lastFailureTimestamp" + ddEntryPointReqsName = "entrypoint.request.total" + ddEntryPointReqDurationName = "entrypoint.request.duration" + ddEntryPointOpenConnsName = "entrypoint.connections.open" + ddOpenConnsName = "service.connections.open" + 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. @@ -41,10 +42,11 @@ func RegisterDatadog(ctx context.Context, config *types.Datadog) Registry { } registry := &standardRegistry{ - configReloadsCounter: datadogClient.NewCounter(ddConfigReloadsName, 1.0), - configReloadsFailureCounter: datadogClient.NewCounter(ddConfigReloadsName, 1.0).With(ddConfigReloadsFailureTagName, "true"), - lastConfigReloadSuccessGauge: datadogClient.NewGauge(ddLastConfigReloadSuccessName), - lastConfigReloadFailureGauge: datadogClient.NewGauge(ddLastConfigReloadFailureName), + configReloadsCounter: datadogClient.NewCounter(ddConfigReloadsName, 1.0), + configReloadsFailureCounter: datadogClient.NewCounter(ddConfigReloadsName, 1.0).With(ddConfigReloadsFailureTagName, "true"), + lastConfigReloadSuccessGauge: datadogClient.NewGauge(ddLastConfigReloadSuccessName), + lastConfigReloadFailureGauge: datadogClient.NewGauge(ddLastConfigReloadFailureName), + tlsCertsNotAfterTimestampGauge: datadogClient.NewGauge(ddTLSCertsNotAfterTimestampName), } if config.AddEntryPointsLabels { diff --git a/pkg/metrics/datadog_test.go b/pkg/metrics/datadog_test.go index e10626e75..af086f725 100644 --- a/pkg/metrics/datadog_test.go +++ b/pkg/metrics/datadog_test.go @@ -36,6 +36,7 @@ func TestDatadog(t *testing.T) { "traefik.entrypoint.request.duration:10000.000000|h|#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.tls.certs.notAfterTimestamp:1.000000|g|#key:value\n", } udp.ShouldReceiveAll(t, expected, func() { @@ -50,5 +51,6 @@ func TestDatadog(t *testing.T) { datadogRegistry.EntryPointReqDurationHistogram().With("entrypoint", "test").Observe(10000) datadogRegistry.EntryPointOpenConnsGauge().With("entrypoint", "test").Set(1) datadogRegistry.ServiceServerUpGauge().With("service", "test", "url", "http://127.0.0.1", "one", "two").Set(1) + datadogRegistry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1) }) } diff --git a/pkg/metrics/influxdb.go b/pkg/metrics/influxdb.go index 6cf33433e..8c6b5b03f 100644 --- a/pkg/metrics/influxdb.go +++ b/pkg/metrics/influxdb.go @@ -26,18 +26,19 @@ type influxDBWriter struct { var influxDBTicker *time.Ticker const ( - influxDBMetricsServiceReqsName = "traefik.service.requests.total" - influxDBMetricsServiceLatencyName = "traefik.service.request.duration" - influxDBRetriesTotalName = "traefik.service.retries.total" - influxDBConfigReloadsName = "traefik.config.reload.total" - influxDBConfigReloadsFailureName = influxDBConfigReloadsName + ".failure" - influxDBLastConfigReloadSuccessName = "traefik.config.reload.lastSuccessTimestamp" - influxDBLastConfigReloadFailureName = "traefik.config.reload.lastFailureTimestamp" - influxDBEntryPointReqsName = "traefik.entrypoint.requests.total" - influxDBEntryPointReqDurationName = "traefik.entrypoint.request.duration" - influxDBEntryPointOpenConnsName = "traefik.entrypoint.connections.open" - influxDBOpenConnsName = "traefik.service.connections.open" - influxDBServerUpName = "traefik.service.server.up" + influxDBMetricsServiceReqsName = "traefik.service.requests.total" + influxDBMetricsServiceLatencyName = "traefik.service.request.duration" + influxDBRetriesTotalName = "traefik.service.retries.total" + influxDBConfigReloadsName = "traefik.config.reload.total" + influxDBConfigReloadsFailureName = influxDBConfigReloadsName + ".failure" + influxDBLastConfigReloadSuccessName = "traefik.config.reload.lastSuccessTimestamp" + influxDBLastConfigReloadFailureName = "traefik.config.reload.lastFailureTimestamp" + influxDBEntryPointReqsName = "traefik.entrypoint.requests.total" + influxDBEntryPointReqDurationName = "traefik.entrypoint.request.duration" + influxDBEntryPointOpenConnsName = "traefik.entrypoint.connections.open" + influxDBOpenConnsName = "traefik.service.connections.open" + influxDBServerUpName = "traefik.service.server.up" + influxDBTLSCertsNotAfterTimestampName = "traefik.tls.certs.notAfterTimestamp" ) const ( @@ -55,10 +56,11 @@ func RegisterInfluxDB(ctx context.Context, config *types.InfluxDB) Registry { } registry := &standardRegistry{ - configReloadsCounter: influxDBClient.NewCounter(influxDBConfigReloadsName), - configReloadsFailureCounter: influxDBClient.NewCounter(influxDBConfigReloadsFailureName), - lastConfigReloadSuccessGauge: influxDBClient.NewGauge(influxDBLastConfigReloadSuccessName), - lastConfigReloadFailureGauge: influxDBClient.NewGauge(influxDBLastConfigReloadFailureName), + configReloadsCounter: influxDBClient.NewCounter(influxDBConfigReloadsName), + configReloadsFailureCounter: influxDBClient.NewCounter(influxDBConfigReloadsFailureName), + lastConfigReloadSuccessGauge: influxDBClient.NewGauge(influxDBLastConfigReloadSuccessName), + lastConfigReloadFailureGauge: influxDBClient.NewGauge(influxDBLastConfigReloadFailureName), + tlsCertsNotAfterTimestampGauge: influxDBClient.NewGauge(influxDBTLSCertsNotAfterTimestampName), } if config.AddEntryPointsLabels { diff --git a/pkg/metrics/influxdb_test.go b/pkg/metrics/influxdb_test.go index 9e0bd1735..ba3db81bd 100644 --- a/pkg/metrics/influxdb_test.go +++ b/pkg/metrics/influxdb_test.go @@ -64,6 +64,16 @@ func TestInfluxDB(t *testing.T) { }) 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) { @@ -121,6 +131,15 @@ func TestInfluxDBHTTP(t *testing.T) { msgEntrypoint := <-c 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) { diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index fe48f4555..a5c28571d 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -21,6 +21,9 @@ type Registry interface { LastConfigReloadSuccessGauge() metrics.Gauge LastConfigReloadFailureGauge() metrics.Gauge + // TLS + TLSCertsNotAfterTimestampGauge() metrics.Gauge + // entry point metrics EntryPointReqsCounter() metrics.Counter EntryPointReqsTLSCounter() metrics.Counter @@ -50,6 +53,7 @@ func NewMultiRegistry(registries []Registry) Registry { var configReloadsFailureCounter []metrics.Counter var lastConfigReloadSuccessGauge []metrics.Gauge var lastConfigReloadFailureGauge []metrics.Gauge + var tlsCertsNotAfterTimestampGauge []metrics.Gauge var entryPointReqsCounter []metrics.Counter var entryPointReqsTLSCounter []metrics.Counter var entryPointReqDurationHistogram []ScalableHistogram @@ -74,6 +78,9 @@ func NewMultiRegistry(registries []Registry) Registry { if r.LastConfigReloadFailureGauge() != nil { lastConfigReloadFailureGauge = append(lastConfigReloadFailureGauge, r.LastConfigReloadFailureGauge()) } + if r.TLSCertsNotAfterTimestampGauge() != nil { + tlsCertsNotAfterTimestampGauge = append(tlsCertsNotAfterTimestampGauge, r.TLSCertsNotAfterTimestampGauge()) + } if r.EntryPointReqsCounter() != nil { entryPointReqsCounter = append(entryPointReqsCounter, r.EntryPointReqsCounter()) } @@ -113,6 +120,7 @@ func NewMultiRegistry(registries []Registry) Registry { configReloadsFailureCounter: multi.NewCounter(configReloadsFailureCounter...), lastConfigReloadSuccessGauge: multi.NewGauge(lastConfigReloadSuccessGauge...), lastConfigReloadFailureGauge: multi.NewGauge(lastConfigReloadFailureGauge...), + tlsCertsNotAfterTimestampGauge: multi.NewGauge(tlsCertsNotAfterTimestampGauge...), entryPointReqsCounter: multi.NewCounter(entryPointReqsCounter...), entryPointReqsTLSCounter: multi.NewCounter(entryPointReqsTLSCounter...), entryPointReqDurationHistogram: NewMultiHistogram(entryPointReqDurationHistogram...), @@ -133,6 +141,7 @@ type standardRegistry struct { configReloadsFailureCounter metrics.Counter lastConfigReloadSuccessGauge metrics.Gauge lastConfigReloadFailureGauge metrics.Gauge + tlsCertsNotAfterTimestampGauge metrics.Gauge entryPointReqsCounter metrics.Counter entryPointReqsTLSCounter metrics.Counter entryPointReqDurationHistogram ScalableHistogram @@ -169,6 +178,10 @@ func (r *standardRegistry) LastConfigReloadFailureGauge() metrics.Gauge { return r.lastConfigReloadFailureGauge } +func (r *standardRegistry) TLSCertsNotAfterTimestampGauge() metrics.Gauge { + return r.tlsCertsNotAfterTimestampGauge +} + func (r *standardRegistry) EntryPointReqsCounter() metrics.Counter { return r.entryPointReqsCounter } diff --git a/pkg/metrics/prometheus.go b/pkg/metrics/prometheus.go index b627b50b4..b042ea850 100644 --- a/pkg/metrics/prometheus.go +++ b/pkg/metrics/prometheus.go @@ -29,6 +29,10 @@ const ( configLastReloadSuccessName = metricConfigPrefix + "last_reload_success" configLastReloadFailureName = metricConfigPrefix + "last_reload_failure" + // TLS. + metricsTLSPrefix = MetricNamePrefix + "tls_" + tlsCertsNotAfterTimestamp = metricsTLSPrefix + "certs_not_after" + // entry point. metricEntryPointPrefix = MetricNamePrefix + "entrypoint_" entryPointReqsTotalName = metricEntryPointPrefix + "requests_total" @@ -121,21 +125,27 @@ func initStandardRegistry(config *types.Prometheus) Registry { Name: configLastReloadFailureName, Help: "Last config reload failure", }, []string{}) + tlsCertsNotAfterTimesptamp := newGaugeFrom(promState.collectors, stdprometheus.GaugeOpts{ + Name: tlsCertsNotAfterTimestamp, + Help: "Certificate expiration timestamp", + }, []string{"cn", "serial", "sans"}) promState.describers = []func(chan<- *stdprometheus.Desc){ configReloads.cv.Describe, configReloadsFailures.cv.Describe, lastConfigReloadSuccess.gv.Describe, lastConfigReloadFailure.gv.Describe, + tlsCertsNotAfterTimesptamp.gv.Describe, } reg := &standardRegistry{ - epEnabled: config.AddEntryPointsLabels, - svcEnabled: config.AddServicesLabels, - configReloadsCounter: configReloads, - configReloadsFailureCounter: configReloadsFailures, - lastConfigReloadSuccessGauge: lastConfigReloadSuccess, - lastConfigReloadFailureGauge: lastConfigReloadFailure, + epEnabled: config.AddEntryPointsLabels, + svcEnabled: config.AddServicesLabels, + configReloadsCounter: configReloads, + configReloadsFailureCounter: configReloadsFailures, + lastConfigReloadSuccessGauge: lastConfigReloadSuccess, + lastConfigReloadFailureGauge: lastConfigReloadFailure, + tlsCertsNotAfterTimestampGauge: tlsCertsNotAfterTimesptamp, } if config.AddEntryPointsLabels { @@ -163,11 +173,13 @@ func initStandardRegistry(config *types.Prometheus) Registry { entryPointReqDurations.hv.Describe, entryPointOpenConns.gv.Describe, }...) + reg.entryPointReqsCounter = entryPointReqs reg.entryPointReqsTLSCounter = entryPointReqsTLS reg.entryPointReqDurationHistogram, _ = NewHistogramWithScale(entryPointReqDurations, time.Second) reg.entryPointOpenConnsGauge = entryPointOpenConns } + if config.AddServicesLabels { serviceReqs := newCounterFrom(promState.collectors, stdprometheus.CounterOpts{ Name: serviceReqsTotalName, diff --git a/pkg/metrics/prometheus_test.go b/pkg/metrics/prometheus_test.go index 87fc1b650..5d5a5e34d 100644 --- a/pkg/metrics/prometheus_test.go +++ b/pkg/metrics/prometheus_test.go @@ -116,6 +116,11 @@ func TestPrometheus(t *testing.T) { prometheusRegistry.LastConfigReloadSuccessGauge().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. EntryPointReqsCounter(). With("code", strconv.Itoa(http.StatusOK), "method", http.MethodGet, "protocol", "http", "entrypoint", "http"). @@ -175,6 +180,15 @@ func TestPrometheus(t *testing.T) { name: configLastReloadFailureName, assert: buildTimestampAssert(t, configLastReloadFailureName), }, + { + name: tlsCertsNotAfterTimestamp, + labels: map[string]string{ + "cn": "value", + "serial": "value", + "sans": "value", + }, + assert: buildTimestampAssert(t, tlsCertsNotAfterTimestamp), + }, { name: entryPointReqsTotalName, labels: map[string]string{ diff --git a/pkg/metrics/statsd.go b/pkg/metrics/statsd.go index 552741f36..3ef6a72ac 100644 --- a/pkg/metrics/statsd.go +++ b/pkg/metrics/statsd.go @@ -17,18 +17,19 @@ var ( ) const ( - statsdMetricsServiceReqsName = "service.request.total" - statsdMetricsServiceLatencyName = "service.request.duration" - statsdRetriesTotalName = "service.retries.total" - statsdConfigReloadsName = "config.reload.total" - statsdConfigReloadsFailureName = statsdConfigReloadsName + ".failure" - statsdLastConfigReloadSuccessName = "config.reload.lastSuccessTimestamp" - statsdLastConfigReloadFailureName = "config.reload.lastFailureTimestamp" - statsdEntryPointReqsName = "entrypoint.request.total" - statsdEntryPointReqDurationName = "entrypoint.request.duration" - statsdEntryPointOpenConnsName = "entrypoint.connections.open" - statsdOpenConnsName = "service.connections.open" - statsdServerUpName = "service.server.up" + statsdMetricsServiceReqsName = "service.request.total" + statsdMetricsServiceLatencyName = "service.request.duration" + statsdRetriesTotalName = "service.retries.total" + statsdConfigReloadsName = "config.reload.total" + statsdConfigReloadsFailureName = statsdConfigReloadsName + ".failure" + statsdLastConfigReloadSuccessName = "config.reload.lastSuccessTimestamp" + statsdLastConfigReloadFailureName = "config.reload.lastFailureTimestamp" + statsdEntryPointReqsName = "entrypoint.request.total" + statsdEntryPointReqDurationName = "entrypoint.request.duration" + statsdEntryPointOpenConnsName = "entrypoint.connections.open" + statsdOpenConnsName = "service.connections.open" + 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. @@ -48,10 +49,11 @@ func RegisterStatsd(ctx context.Context, config *types.Statsd) Registry { } registry := &standardRegistry{ - configReloadsCounter: statsdClient.NewCounter(statsdConfigReloadsName, 1.0), - configReloadsFailureCounter: statsdClient.NewCounter(statsdConfigReloadsFailureName, 1.0), - lastConfigReloadSuccessGauge: statsdClient.NewGauge(statsdLastConfigReloadSuccessName), - lastConfigReloadFailureGauge: statsdClient.NewGauge(statsdLastConfigReloadFailureName), + configReloadsCounter: statsdClient.NewCounter(statsdConfigReloadsName, 1.0), + configReloadsFailureCounter: statsdClient.NewCounter(statsdConfigReloadsFailureName, 1.0), + lastConfigReloadSuccessGauge: statsdClient.NewGauge(statsdLastConfigReloadSuccessName), + lastConfigReloadFailureGauge: statsdClient.NewGauge(statsdLastConfigReloadFailureName), + tlsCertsNotAfterTimestampGauge: statsdClient.NewGauge(statsdTLSCertsNotAfterTimestampName), } if config.AddEntryPointsLabels { diff --git a/pkg/metrics/statsd_test.go b/pkg/metrics/statsd_test.go index eb3af9e5d..3b8deca06 100644 --- a/pkg/metrics/statsd_test.go +++ b/pkg/metrics/statsd_test.go @@ -35,6 +35,7 @@ func TestStatsD(t *testing.T) { "traefik.entrypoint.request.duration:10000.000000|ms", "traefik.entrypoint.connections.open:1.000000|g\n", "traefik.service.server.up:1.000000|g\n", + "tls.certs.notAfterTimestamp:1.000000|g\n", } udp.ShouldReceiveAll(t, expected, func() { @@ -49,6 +50,7 @@ func TestStatsD(t *testing.T) { statsdRegistry.EntryPointReqDurationHistogram().With("entrypoint", "test").Observe(10000) statsdRegistry.EntryPointOpenConnsGauge().With("entrypoint", "test").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.connections.open:1.000000|g\n", "testPrefix.service.server.up:1.000000|g\n", + "tls.certs.notAfterTimestamp:1.000000|g\n", } udp.ShouldReceiveAll(t, expected, func() { @@ -89,5 +92,6 @@ func TestStatsDWithPrefix(t *testing.T) { statsdRegistry.EntryPointReqDurationHistogram().With("entrypoint", "test").Observe(10000) statsdRegistry.EntryPointOpenConnsGauge().With("entrypoint", "test").Set(1) statsdRegistry.ServiceServerUpGauge().With("service:test", "url", "http://127.0.0.1").Set(1) + statsdRegistry.TLSCertsNotAfterTimestampGauge().With("key", "value").Set(1) }) } diff --git a/pkg/tls/certificate_store.go b/pkg/tls/certificate_store.go index 2db8afd1c..91bd00ef5 100644 --- a/pkg/tls/certificate_store.go +++ b/pkg/tls/certificate_store.go @@ -56,15 +56,16 @@ func (c CertificateStore) getDefaultCertificateDomains() []string { // GetAllDomains return a slice with all the certificate domain. func (c CertificateStore) GetAllDomains() []string { - allCerts := c.getDefaultCertificateDomains() + allDomains := c.getDefaultCertificateDomains() // Get dynamic certificates if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil { - for domains := range c.DynamicCerts.Get().(map[string]*tls.Certificate) { - allCerts = append(allCerts, domains) + for domain := range c.DynamicCerts.Get().(map[string]*tls.Certificate) { + allDomains = append(allDomains, domain) } } - return allCerts + + return allDomains } // GetBestCertificate returns the best match certificate, and caches the response. diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index 1beebfc64..88fae8357 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -131,6 +131,27 @@ func (m *Manager) Get(storeName, configName string) (*tls.Config, error) { 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 { _, ok := m.stores[storeName] if !ok {