8000 feat(metrics): option to disable default Go/process/promhttp collectors by Tyagiquamar · Pull Request #2116 · Altinity/clickhouse-operator · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

< 8000 /file-filter>
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/prometheus_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,28 @@ So, what we need in general, is to point Prometheus to gather data from:
http://<service/clickhouse-operator-metrics>:8888/metrics
```

### Disabling the default Go, process and promhttp collectors

Both metrics endpoints expose the Prometheus Go client library's default
collectors: `go_*` and `process_*` on the operator and the exporter, plus
`promhttp_*` on the exporter. Set `OPERATOR_METRICS_DISABLE_DEFAULT_COLLECTORS`
to `true` to serve only the component's own metrics. The variable is read by
each process, so set it on both containers to opt both endpoints out:

```yaml
operator:
env:
- name: OPERATOR_METRICS_DISABLE_DEFAULT_COLLECTORS
value: "true"
metrics:
env:
- name: OPERATOR_METRICS_DISABLE_DEFAULT_COLLECTORS
value: "true"
```

Unset, or any value `strconv.ParseBool` rejects, keeps the current behaviour
with the default collectors exposed.

## Setup Prometheus and integrate it with clickhouse-operator
In case we do not have Prometheus available, we can setup it directly into k8s and integrate with `clickhouse-operator`

Expand Down
24 changes: 24 additions & 0 deletions pkg/apis/deployment/env_vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

package deployment

import (
"os"
"strconv"
)

const (
// OPERATOR_POD_NODE_NAME name from spec.nodeName Ex.: ip-172-20-52-62.ec2.internal
OPERATOR_POD_NODE_NAME = "OPERATOR_POD_NODE_NAME"
Expand Down Expand Up @@ -44,6 +49,12 @@ const (
// OPERATOR_K8S_CLIENT_BURST_LIMIT specifies an override for the default k8s client QPS burst limit.
OPERATOR_K8S_CLIENT_BURST_LIMIT = "OPERATOR_K8S_CLIENT_BURST_LIMIT"

// OPERATOR_METRICS_DISABLE_DEFAULT_COLLECTORS opts the operator and the
// metrics exporter out of the Prometheus client library's default Go
// runtime, process and promhttp collectors. Unset (the default) keeps them
// exposed; any value accepted by strconv.ParseBool enables the opt-out.
OPERATOR_METRICS_DISABLE_DEFAULT_COLLECTORS = "OPERATOR_METRICS_DISABLE_DEFAULT_COLLECTORS"

// WATCH_NAMESPACE and WATCH_NAMESPACES specifies what namespaces to watch
WATCH_NAMESPACE = "WATCH_NAMESPACE"
// WATCH_NAMESPACES and WATCH_NAMESPACE specifies what namespaces to watch
Expand All @@ -54,3 +65,16 @@ const (
// CHOP_CONFIG path to clickhouse operator configuration file
CHOP_CONFIG = "CHOP_CONFIG"
)

// DefaultCollectorsDisabled reports whether the metrics endpoints should stop
// exposing the default Go runtime, process and promhttp collectors.
func DefaultCollectorsDisabled() bool {
disabled, err := strconv.ParseBool(os.Getenv(OPERATOR_METRICS_DISABLE_DEFAULT_COLLECTORS))
if err != nil {
// Opt-out only: an unset or malformed value must leave the default
// (collectors exposed) in place, so a typo cannot silently drop
// runtime series that existing dashboards and alerts scrape.
return false
}
return disabled
}
46 changes: 46 additions & 0 deletions pkg/apis/deployment/env_vars_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package deployment

import (
"testing"

"github.com/stretchr/testify/require"
)

// TestDefaultCollectorsDisabled pins the opt-out semantics: only a value that
// strconv.ParseBool understands as true may remove the collectors, so an unset
// or malformed value leaves the long-standing default behaviour untouched.
func TestDefaultCollectorsDisabled(t *testing.T) {
tests := []struct {
name string
value string
expected bool
}{
{name: "unset", value: "", expected: false},
{name: "false", value: "false", expected: false},
{name: "zero", value: "0", expected: false},
{name: "true", value: "true", expected: true},
{name: "one", value: "1", expected: true},
{name: "malformed", value: "yes", expected: false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(OPERATOR_METRICS_DISABLE_DEFAULT_COLLECTORS, tt.value)
require.Equal(t, tt.expected, DefaultCollectorsDisabled())
})
}
}
20 changes: 18 additions & 2 deletions pkg/metrics/clickhouse/rest_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"

api "github.com/altinity/clickhouse-operator/pkg/apis/clickhouse.altinity.com/v1"
"github.com/altinity/clickhouse-operator/pkg/apis/deployment"
"github.com/altinity/clickhouse-operator/pkg/apis/metrics"
)

Expand Down Expand Up @@ -156,6 +157,22 @@ func (s *RESTServer) decodeRequest(r *http.Request) (*RESTRequest, error) {
return req, nil
}

// newMetricsHandler registers the exporter and returns the handler that serves
// its metrics. The default keeps the previous wiring: the global registry,
// which already carries the Go runtime and process collectors, plus
// promhttp.Handler(), which adds the promhttp_* handler metrics. With the
// default collectors disabled the exporter goes into a private registry, so a
// scrape carries the ClickHouse series and nothing else.
func newMetricsHandler(exporter prometheus.Collector) http.Handler {
if deployment.DefaultCollectorsDisabled() {
registry := prometheus.NewRegistry()
registry.MustRegister(exporter)
return promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
}
prometheus.MustRegister(exporter)
return promhttp.Handler()
}

// StartMetricsREST starts Prometheus metrics exporter and REST API server
func StartMetricsREST(
metricsAddress string,
Expand All @@ -171,7 +188,6 @@ func StartMetricsREST(

// Create and register Prometheus exporter
exporter := NewExporter(registry, collectorTimeout)
prometheus.MustRegister(exporter)

// Create REST server
restServer := NewRESTServer(registry)
Expand All @@ -194,7 +210,7 @@ func StartMetricsREST(
}

// Setup HTTP handlers
http.Handle(metricsPath, promhttp.Handler())
http.Handle(metricsPath, newMetricsHandler(exporter))
http.Handle(chiListPath, chiHandler)

// Start HTTP servers. Prometheus /metrics ALWAYS binds the original address
Expand Down
123 changes: 123 additions & 0 deletions pkg/metrics/clickhouse/rest_server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package clickhouse

import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"

"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"

"github.com/altinity/clickhouse-operator/pkg/apis/deployment"
)

var probeSeq atomic.Int64

// probeExporter stands in for *Exporter: one self-described series that must
// survive whichever registry serves the scrape, without touching ClickHouse.
type probeExporter struct {
name string
desc *prometheus.Desc
}

func newProbeExporter() *probeExporter {
name := fmt.Sprintf("clickhouse_metrics_exporter_probe_%d", probeSeq.Add(1))
return &probeExporter{
name: name,
desc: prometheus.NewDesc(name, "probe metric", nil, nil),
}
}

func (e *probeExporter) Describe(ch chan<- *prometheus.Desc) {
ch <- e.desc
}

func (e *probeExporter) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(e.desc, prometheus.GaugeValue, 1)
}

// scrapeFamilyNames serves one scrape and returns the metric family names in
// the payload, which is what a Prometheus server would ingest.
func scrapeFamilyNames(t *testing.T, handler http.Handler) map[string]bool {
t.Helper()

rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil))
require.Equal(t, http.StatusOK, rec.Code)

names := map[string]bool{}
for _, line := range strings.Split(rec.Body.String(), "\n") {
rest, ok := strings.CutPrefix(line, "# TYPE ")
if !ok {
continue
}
names[strings.Fields(rest)[0]] = true
}
return names
}

func requireNoFamilyPrefix(t *testing.T, names map[string]bool, prefixes ...string) {
t.Helper()

for name := range names {
for _, prefix := range prefixes {
require.False(t, strings.HasPrefix(name, prefix),
"%s is exposed with the default collectors disabled", name)
}
}
}

// TestMetricsHandlerCollectorsToggle is the exporter-side half of issue #2098.
// The default scrape keeps go_*, process_* and the promhttp_* handler metrics;
// the opt-out leaves the ClickHouse series and nothing else.
func TestMetricsHandlerCollectorsToggle(t *testing.T) {
t.Run("default keeps runtime, process and promhttp collectors", func(t *testing.T) {
t.Setenv(deployment.OPERATOR_METRICS_DISABLE_DEFAULT_COLLECTORS, "")

probe := newProbeExporter()
names := scrapeFamilyNames(t, newMetricsHandler(probe))

require.True(t, names["go_goroutines"],
"go_* is missing by default - the global registry is no longer served")
require.True(t, names["process_start_time_seconds"],
"process_* is missing by default - the global registry is no longer served")

instrumented := false
for name := range names {
if strings.HasPrefix(name, "promhttp_") {
instrumented = true
}
}
require.True(t, instrumented,
"promhttp_* is missing by default - the handler is no longer instrumented")
require.True(t, names[probe.name], "the exporter's own metric is missing")
})

t.Run("opt-out drops them and keeps exporter metrics", func(t *testing.T) {
t.Setenv(deployment.OPERATOR_METRICS_DISABLE_DEFAULT_COLLECTORS, "true")

probe := newProbeExporter()
names := scrapeFamilyNames(t, newMetricsHandler(probe))

requireNoFamilyPrefix(t, names, "go_", "process_", "promhttp_")
require.True(t, names[probe.name],
"the exporter's own metric is missing - the opt-out must only drop default collectors")
})
}
109 changes: 109 additions & 0 deletions pkg/metrics/operator/default_collectors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package operator

import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"

prom "github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)

var probeSeq atomic.Int64

// probeCollector stands in for the operator's OTel exporter: one self-described
// series that must survive whichever registry serves the scrape.
type probeCollector struct {
name string
desc *prom.Desc
}

func newProbeCollector() *probeCollector {
name := fmt.Sprintf("clickhouse_operator_collectors_probe_%d", probeSeq.Add(1))
return &probeCollector{
name: name,
desc: prom.NewDesc(name, "probe metric", nil, nil),
}
}

func (c *probeCollector) Describe(ch chan<- *prom.Desc) {
ch <- c.desc
}

func (c *probeCollector) Collect(ch chan<- prom.Metric) {
ch <- prom.MustNewConstMetric(c.desc, prom.GaugeValue, 1)
}

// scrapeFamilyNames serves one scrape and returns the metric family names in
// the payload, which is what a Prometheus server would ingest.
func scrapeFamilyNames(t *testing.T, handler http.Handler) map[string]bool {
t.Helper()

rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil))
require.Equal(t, http.StatusOK, rec.Code)

names := map[string]bool{}
for _, line := range strings.Split(rec.Body.String(), "\n") {
rest, ok := strings.CutPrefix(line, "# TYPE ")
if !ok {
continue
}
names[strings.Fields(rest)[0]] = true
}
return names
}

func requireNoFamilyPrefix(t *testing.T, names map[string]bool, prefixes ...string) {
t.Helper()

for name := range names {
for _, prefix := range prefixes {
require.False(t, strings.HasPrefix(name, prefix),
"%s is exposed with the default collectors disabled", name)
}
}
}

// TestMetricsCollectorsToggle is the regression guard for issue #2098: the
// operator endpoint must keep exposing go_*/process_* by default, and must drop
// them - without dropping the operator's own series - when the opt-out is set.
func TestMetricsCollectorsToggle(t *testing.T) {
t.Run("default keeps runtime and process collectors", func(t *testing.T) {
names := scrapeFamilyNames(t, newMetricsHandler(newMetricsRegistry(false).gatherer))

require.True(t, names["go_goroutines"],
"go_* is missing by default - the default registry is no longer served")
require.True(t, names["process_start_time_seconds"],
"process_* is missing by default - the default registry is no longer served")
})

t.Run("opt-out drops them and keeps operator metrics", func(t *testing.T) {
probe := newProbeCollector()
registry := newMetricsRegistry(true)
registry.registerer.MustRegister(probe)

names := scrapeFamilyNames(t, newMetricsHandler(registry.gatherer))

requireNoFamilyPrefix(t, names, "go_", "process_", "promhttp_")
require.True(t, names[probe.name],
"the operator's own metric is missing - the opt-out must only drop default collectors")
})
}
Loading
Loading
0