mirror of https://github.com/grafana/loki
Creating telemetry metrics for loki operator based on complete build spec (#32)
parent
7045c92086
commit
8415b37568
@ -0,0 +1,161 @@ |
||||
package metrics |
||||
|
||||
import ( |
||||
"reflect" |
||||
|
||||
"github.com/prometheus/client_golang/prometheus" |
||||
"sigs.k8s.io/controller-runtime/pkg/metrics" |
||||
|
||||
lokiv1beta1 "github.com/ViaQ/loki-operator/api/v1beta1" |
||||
"github.com/ViaQ/loki-operator/internal/manifests" |
||||
) |
||||
|
||||
// UserDefinedLimitsType defines a label that describes the type of limits
|
||||
// imposed on the cluster
|
||||
type UserDefinedLimitsType string |
||||
|
||||
const ( |
||||
labelGlobal UserDefinedLimitsType = "global" |
||||
labelTenant UserDefinedLimitsType = "tenant" |
||||
) |
||||
|
||||
var ( |
||||
deploymentMetric = prometheus.NewGaugeVec( |
||||
prometheus.GaugeOpts{ |
||||
Name: "lokistack_deployments", |
||||
Help: "Number of clusters that are deployed", |
||||
}, |
||||
[]string{"size", "stack_id"}, |
||||
) |
||||
|
||||
userDefinedLimitsMetric = prometheus.NewGaugeVec( |
||||
prometheus.GaugeOpts{ |
||||
Name: "lokistack_user_defined_limits", |
||||
Help: "Number of clusters that are using user defined limits", |
||||
}, |
||||
[]string{"size", "stack_id", "type"}, |
||||
) |
||||
|
||||
globalStreamLimitMetric = prometheus.NewGaugeVec( |
||||
prometheus.GaugeOpts{ |
||||
Name: "lokistack_global_stream_limit", |
||||
Help: "Sum of stream limits used globally by the ingesters", |
||||
}, |
||||
[]string{"size", "stack_id"}, |
||||
) |
||||
|
||||
averageTenantStreamLimitMetric = prometheus.NewGaugeVec( |
||||
prometheus.GaugeOpts{ |
||||
Name: "lokistack_avg_stream_limit_per_tenant", |
||||
Help: "Sum of stream limits used for defined tenants by the ingesters", |
||||
}, |
||||
[]string{"size", "stack_id"}, |
||||
) |
||||
) |
||||
|
||||
// RegisterMetricCollectors registers the prometheus collectors with the k8 default metrics
|
||||
func RegisterMetricCollectors() { |
||||
metricCollectors := []prometheus.Collector{ |
||||
deploymentMetric, |
||||
userDefinedLimitsMetric, |
||||
globalStreamLimitMetric, |
||||
averageTenantStreamLimitMetric, |
||||
} |
||||
|
||||
for _, collector := range metricCollectors { |
||||
metrics.Registry.MustRegister(collector) |
||||
} |
||||
} |
||||
|
||||
// Collect takes metrics based on the spec
|
||||
func Collect(spec *lokiv1beta1.LokiStackSpec, stackName string) { |
||||
defaultSpec := manifests.DefaultLokiStackSpec(spec.Size) |
||||
sizes := []lokiv1beta1.LokiStackSizeType{lokiv1beta1.SizeOneXSmall, lokiv1beta1.SizeOneXMedium} |
||||
|
||||
for _, size := range sizes { |
||||
var ( |
||||
globalRate float64 = 0 |
||||
tenantRate float64 = 0 |
||||
isUsingSize = false |
||||
isUsingTenantLimits = false |
||||
isUsingCustomGlobalLimits = false |
||||
) |
||||
|
||||
if spec.Size == size { |
||||
isUsingSize = true |
||||
|
||||
if !reflect.DeepEqual(spec.Limits.Global, defaultSpec.Limits.Global) { |
||||
isUsingCustomGlobalLimits = true |
||||
} |
||||
|
||||
if len(spec.Limits.Tenants) != 0 { |
||||
isUsingTenantLimits = true |
||||
} |
||||
|
||||
if ingesters := spec.Template.Ingester.Replicas; ingesters > 0 { |
||||
tenantRate = streamRate(spec.Limits.Tenants, ingesters) |
||||
globalRate = float64(spec.Limits.Global.IngestionLimits.MaxGlobalStreamsPerTenant / ingesters) |
||||
} |
||||
} |
||||
|
||||
setDeploymentMetric(size, stackName, isUsingSize) |
||||
setUserDefinedLimitsMetric(size, stackName, labelGlobal, isUsingCustomGlobalLimits) |
||||
setUserDefinedLimitsMetric(size, stackName, labelTenant, isUsingTenantLimits) |
||||
setGlobalStreamLimitMetric(size, stackName, globalRate) |
||||
setAverageTenantStreamLimitMetric(size, stackName, tenantRate) |
||||
} |
||||
} |
||||
|
||||
func setDeploymentMetric(size lokiv1beta1.LokiStackSizeType, identifier string, active bool) { |
||||
deploymentMetric.With(prometheus.Labels{ |
||||
"size": string(size), |
||||
"stack_id": identifier, |
||||
}).Set(boolValue(active)) |
||||
} |
||||
|
||||
func setUserDefinedLimitsMetric(size lokiv1beta1.LokiStackSizeType, identifier string, limitType UserDefinedLimitsType, active bool) { |
||||
userDefinedLimitsMetric.With(prometheus.Labels{ |
||||
"size": string(size), |
||||
"stack_id": identifier, |
||||
"type": string(limitType), |
||||
}).Set(boolValue(active)) |
||||
} |
||||
|
||||
func setGlobalStreamLimitMetric(size lokiv1beta1.LokiStackSizeType, identifier string, rate float64) { |
||||
globalStreamLimitMetric.With(prometheus.Labels{ |
||||
"size": string(size), |
||||
"stack_id": identifier, |
||||
}).Set(rate) |
||||
} |
||||
|
||||
func setAverageTenantStreamLimitMetric(size lokiv1beta1.LokiStackSizeType, identifier string, rate float64) { |
||||
averageTenantStreamLimitMetric.With(prometheus.Labels{ |
||||
"size": string(size), |
||||
"stack_id": identifier, |
||||
}).Set(rate) |
||||
} |
||||
|
||||
func boolValue(value bool) float64 { |
||||
if value { |
||||
return 1 |
||||
} |
||||
return 0 |
||||
} |
||||
|
||||
func streamRate(tenantLimits map[string]lokiv1beta1.LimitsTemplateSpec, ingesters int32) float64 { |
||||
var tenants, tenantStreamLimit int32 = 0, 0 |
||||
|
||||
for _, tenant := range tenantLimits { |
||||
if tenant.IngestionLimits == nil || tenant.IngestionLimits.MaxGlobalStreamsPerTenant == 0 { |
||||
continue |
||||
} |
||||
|
||||
tenants++ |
||||
tenantStreamLimit += tenant.IngestionLimits.MaxGlobalStreamsPerTenant |
||||
} |
||||
|
||||
if tenants == 0 || ingesters == 0 { |
||||
return 0 |
||||
} |
||||
return float64(tenantStreamLimit / ingesters / tenants) |
||||
} |
@ -1 +0,0 @@ |
||||
.DS_Store |
@ -1,20 +0,0 @@ |
||||
language: go |
||||
go: 1.13 |
||||
|
||||
branches: |
||||
only: |
||||
- master |
||||
|
||||
env: |
||||
global: |
||||
# CODECOV_TOKEN |
||||
- secure: "O4Hay0TPFW2eq1+Y9vlW4W8VIY9QlKzGgEoL8mgbHQ67RrvqaB7Ft6kdmUaVztu9a5ZB4QwwU8dCFV6D/+dY710qqayQghY2r4igqExzkMyRa3InQtGjhYd9CVpjbR5cXYV1PL9h/Jf3PTTUCmSJp0tpqZCce4BezgnhdSjccGzx3wlzj66+bDdpCzRkWtCvYvI4Fxg5aM5kBke1Ti+aLdEQNzDpkbM38/iyUQDuM+y6ZO+AuP9zqdMY82O6yEMXJppPtVnfqhTJQyDiEF6J6h3TauOb9nKhu8Eg0d2b0HseTRVcCigH3usCm6WYmqhInmsbdAge9gSs9SdYq06VYghG/AvnuRNbmGn3DHuRelyH0gBuXrzZxNP9nfPego4bvk6jQvPSu/flB7JHixkDkFBCO1b2R3ZdOdT6fg+qPQBMxEGy3YJ6ylZ0oqC0AByC+0OP7Hc/xv5U4uqnJ5oBVt4yt26sEjlMs8piXnkoNDoXVjgjfpqLLMyuSRzL1nbf7P270E7L0hDzWInObWlM7FuAl6ghb4j20jvx+4C5UKb8JJPMdbEOcDWZuJOnpThpcNIeP08Xks1wDJ5R01cxK2gja8Hmg+nF320bHybn3RRyOke7tuC3Egez2QaKEoQl9YmcepO+TENcsFk86v8Le68UHi8mJps5mm7iuJb2xyQ=" |
||||
|
||||
before_install: |
||||
- go get -u golang.org/x/lint/golint |
||||
|
||||
script: |
||||
- make ci-test |
||||
|
||||
after_success: |
||||
- bash <(curl -s https://codecov.io/bash) |
@ -1,22 +0,0 @@ |
||||
Copyright (c) 2017-present Yuki Iwanaga |
||||
|
||||
MIT License |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining |
||||
a copy of this software and associated documentation files (the |
||||
"Software"), to deal in the Software without restriction, including |
||||
without limitation the rights to use, copy, modify, merge, publish, |
||||
distribute, sublicense, and/or sell copies of the Software, and to |
||||
permit persons to whom the Software is furnished to do so, subject to |
||||
the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be |
||||
included in all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
@ -1,30 +0,0 @@ |
||||
SHELL := /bin/bash -eu -o pipefail
|
||||
|
||||
GO_TEST_FLAGS := -v
|
||||
|
||||
PACKAGE_DIRS := $(shell go list ./... 2> /dev/null | grep -v /vendor/)
|
||||
SRC_FILES := $(shell find . -name '*.go' -not -path './vendor/*')
|
||||
|
||||
|
||||
# Tasks
|
||||
#-----------------------------------------------
|
||||
.PHONY: lint |
||||
lint: |
||||
@gofmt -e -d -s $(SRC_FILES) | awk '{ e = 1; print $0 } END { if (e) exit(1) }'
|
||||
@echo $(SRC_FILES) | xargs -n1 golint -set_exit_status
|
||||
@go vet $(PACKAGE_DIRS)
|
||||
|
||||
.PHONY: test |
||||
test: lint |
||||
@go test $(GO_TEST_FLAGS) $(PACKAGE_DIRS)
|
||||
|
||||
.PHONY: ci-test |
||||
ci-test: lint |
||||
@echo > coverage.txt
|
||||
@for d in $(PACKAGE_DIRS); do \
|
||||
go test -coverprofile=profile.out -covermode=atomic -race -v $$d; \
|
||||
if [ -f profile.out ]; then \
|
||||
cat profile.out >> coverage.txt; \
|
||||
rm profile.out; \
|
||||
fi; \
|
||||
done
|
@ -1,69 +0,0 @@ |
||||
defaults |
||||
======== |
||||
|
||||
[](https://travis-ci.org/creasty/defaults) |
||||
[](https://codecov.io/gh/creasty/defaults) |
||||
[](https://github.com/creasty/defaults/releases) |
||||
[](./LICENSE) |
||||
|
||||
Initialize structs with default values |
||||
|
||||
- Supports almost all kind of types |
||||
- Scalar types |
||||
- `int/8/16/32/64`, `uint/8/16/32/64`, `float32/64` |
||||
- `uintptr`, `bool`, `string` |
||||
- Complex types |
||||
- `map`, `slice`, `struct` |
||||
- Aliased types |
||||
- `time.Duration` |
||||
- e.g., `type Enum string` |
||||
- Pointer types |
||||
- e.g., `*SampleStruct`, `*int` |
||||
- Recursively initializes fields in a struct |
||||
- Dynamically sets default values by [`defaults.Setter`](./setter.go) interface |
||||
- Preserves non-initial values from being reset with a default value |
||||
|
||||
|
||||
Usage |
||||
----- |
||||
|
||||
```go |
||||
type Gender string |
||||
|
||||
type Sample struct { |
||||
Name string `default:"John Smith"` |
||||
Age int `default:"27"` |
||||
Gender Gender `default:"m"` |
||||
|
||||
Slice []string `default:"[]"` |
||||
SliceByJSON []int `default:"[1, 2, 3]"` // Supports JSON |
||||
Map map[string]int `default:"{}"` |
||||
MapByJSON map[string]int `default:"{\"foo\": 123}"` |
||||
|
||||
Struct OtherStruct `default:"{}"` |
||||
StructPtr *OtherStruct `default:"{\"Foo\": 123}"` |
||||
|
||||
NoTag OtherStruct // Recurses into a nested struct by default |
||||
OptOut OtherStruct `default:"-"` // Opt-out |
||||
} |
||||
|
||||
type OtherStruct struct { |
||||
Hello string `default:"world"` // Tags in a nested struct also work |
||||
Foo int `default:"-"` |
||||
Random int `default:"-"` |
||||
} |
||||
|
||||
// SetDefaults implements defaults.Setter interface |
||||
func (s *OtherStruct) SetDefaults() { |
||||
if defaults.CanUpdate(s.Random) { // Check if it's a zero value (recommended) |
||||
s.Random = rand.Int() // Set a dynamic value |
||||
} |
||||
} |
||||
``` |
||||
|
||||
```go |
||||
obj := &Sample{} |
||||
if err := defaults.Set(obj); err != nil { |
||||
panic(err) |
||||
} |
||||
``` |
@ -1,195 +0,0 @@ |
||||
package defaults |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"reflect" |
||||
"strconv" |
||||
"time" |
||||
) |
||||
|
||||
var ( |
||||
errInvalidType = errors.New("not a struct pointer") |
||||
) |
||||
|
||||
const ( |
||||
fieldName = "default" |
||||
) |
||||
|
||||
// Set initializes members in a struct referenced by a pointer.
|
||||
// Maps and slices are initialized by `make` and other primitive types are set with default values.
|
||||
// `ptr` should be a struct pointer
|
||||
func Set(ptr interface{}) error { |
||||
if reflect.TypeOf(ptr).Kind() != reflect.Ptr { |
||||
return errInvalidType |
||||
} |
||||
|
||||
v := reflect.ValueOf(ptr).Elem() |
||||
t := v.Type() |
||||
|
||||
if t.Kind() != reflect.Struct { |
||||
return errInvalidType |
||||
} |
||||
|
||||
for i := 0; i < t.NumField(); i++ { |
||||
if defaultVal := t.Field(i).Tag.Get(fieldName); defaultVal != "-" { |
||||
if err := setField(v.Field(i), defaultVal); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
callSetter(ptr) |
||||
return nil |
||||
} |
||||
|
||||
// MustSet function is a wrapper of Set function
|
||||
// It will call Set and panic if err not equals nil.
|
||||
func MustSet(ptr interface{}) { |
||||
if err := Set(ptr); err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
|
||||
func setField(field reflect.Value, defaultVal string) error { |
||||
if !field.CanSet() { |
||||
return nil |
||||
} |
||||
|
||||
if !shouldInitializeField(field, defaultVal) { |
||||
return nil |
||||
} |
||||
|
||||
if isInitialValue(field) { |
||||
switch field.Kind() { |
||||
case reflect.Bool: |
||||
if val, err := strconv.ParseBool(defaultVal); err == nil { |
||||
field.Set(reflect.ValueOf(val).Convert(field.Type())) |
||||
} |
||||
case reflect.Int: |
||||
if val, err := strconv.ParseInt(defaultVal, 0, strconv.IntSize); err == nil { |
||||
field.Set(reflect.ValueOf(int(val)).Convert(field.Type())) |
||||
} |
||||
case reflect.Int8: |
||||
if val, err := strconv.ParseInt(defaultVal, 0, 8); err == nil { |
||||
field.Set(reflect.ValueOf(int8(val)).Convert(field.Type())) |
||||
} |
||||
case reflect.Int16: |
||||
if val, err := strconv.ParseInt(defaultVal, 0, 16); err == nil { |
||||
field.Set(reflect.ValueOf(int16(val)).Convert(field.Type())) |
||||
} |
||||
case reflect.Int32: |
||||
if val, err := strconv.ParseInt(defaultVal, 0, 32); err == nil { |
||||
field.Set(reflect.ValueOf(int32(val)).Convert(field.Type())) |
||||
} |
||||
case reflect.Int64: |
||||
if val, err := time.ParseDuration(defaultVal); err == nil { |
||||
field.Set(reflect.ValueOf(val).Convert(field.Type())) |
||||
} else if val, err := strconv.ParseInt(defaultVal, 0, 64); err == nil { |
||||
field.Set(reflect.ValueOf(val).Convert(field.Type())) |
||||
} |
||||
case reflect.Uint: |
||||
if val, err := strconv.ParseUint(defaultVal, 0, strconv.IntSize); err == nil { |
||||
field.Set(reflect.ValueOf(uint(val)).Convert(field.Type())) |
||||
} |
||||
case reflect.Uint8: |
||||
if val, err := strconv.ParseUint(defaultVal, 0, 8); err == nil { |
||||
field.Set(reflect.ValueOf(uint8(val)).Convert(field.Type())) |
||||
} |
||||
case reflect.Uint16: |
||||
if val, err := strconv.ParseUint(defaultVal, 0, 16); err == nil { |
||||
field.Set(reflect.ValueOf(uint16(val)).Convert(field.Type())) |
||||
} |
||||
case reflect.Uint32: |
||||
if val, err := strconv.ParseUint(defaultVal, 0, 32); err == nil { |
||||
field.Set(reflect.ValueOf(uint32(val)).Convert(field.Type())) |
||||
} |
||||
case reflect.Uint64: |
||||
if val, err := strconv.ParseUint(defaultVal, 0, 64); err == nil { |
||||
field.Set(reflect.ValueOf(val).Convert(field.Type())) |
||||
} |
||||
case reflect.Uintptr: |
||||
if val, err := strconv.ParseUint(defaultVal, 0, strconv.IntSize); err == nil { |
||||
field.Set(reflect.ValueOf(uintptr(val)).Convert(field.Type())) |
||||
} |
||||
case reflect.Float32: |
||||
if val, err := strconv.ParseFloat(defaultVal, 32); err == nil { |
||||
field.Set(reflect.ValueOf(float32(val)).Convert(field.Type())) |
||||
} |
||||
case reflect.Float64: |
||||
if val, err := strconv.ParseFloat(defaultVal, 64); err == nil { |
||||
field.Set(reflect.ValueOf(val).Convert(field.Type())) |
||||
} |
||||
case reflect.String: |
||||
field.Set(reflect.ValueOf(defaultVal).Convert(field.Type())) |
||||
|
||||
case reflect.Slice: |
||||
ref := reflect.New(field.Type()) |
||||
ref.Elem().Set(reflect.MakeSlice(field.Type(), 0, 0)) |
||||
if defaultVal != "" && defaultVal != "[]" { |
||||
if err := json.Unmarshal([]byte(defaultVal), ref.Interface()); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
field.Set(ref.Elem().Convert(field.Type())) |
||||
case reflect.Map: |
||||
ref := reflect.New(field.Type()) |
||||
ref.Elem().Set(reflect.MakeMap(field.Type())) |
||||
if defaultVal != "" && defaultVal != "{}" { |
||||
if err := json.Unmarshal([]byte(defaultVal), ref.Interface()); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
field.Set(ref.Elem().Convert(field.Type())) |
||||
case reflect.Struct: |
||||
if defaultVal != "" && defaultVal != "{}" { |
||||
if err := json.Unmarshal([]byte(defaultVal), field.Addr().Interface()); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
case reflect.Ptr: |
||||
field.Set(reflect.New(field.Type().Elem())) |
||||
} |
||||
} |
||||
|
||||
switch field.Kind() { |
||||
case reflect.Ptr: |
||||
setField(field.Elem(), defaultVal) |
||||
callSetter(field.Interface()) |
||||
case reflect.Struct: |
||||
if err := Set(field.Addr().Interface()); err != nil { |
||||
return err |
||||
} |
||||
case reflect.Slice: |
||||
for j := 0; j < field.Len(); j++ { |
||||
if err := setField(field.Index(j), defaultVal); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func isInitialValue(field reflect.Value) bool { |
||||
return reflect.DeepEqual(reflect.Zero(field.Type()).Interface(), field.Interface()) |
||||
} |
||||
|
||||
func shouldInitializeField(field reflect.Value, tag string) bool { |
||||
switch field.Kind() { |
||||
case reflect.Struct: |
||||
return true |
||||
case reflect.Ptr: |
||||
if !field.IsNil() && field.Elem().Kind() == reflect.Struct { |
||||
return true |
||||
} |
||||
case reflect.Slice: |
||||
return field.Len() > 0 || tag != "" |
||||
} |
||||
|
||||
return tag != "" |
||||
} |
||||
|
||||
// CanUpdate returns true when the given value is an initial value of its type
|
||||
func CanUpdate(v interface{}) bool { |
||||
return isInitialValue(reflect.ValueOf(v)) |
||||
} |
@ -1,3 +0,0 @@ |
||||
module github.com/creasty/defaults |
||||
|
||||
go 1.13 |
@ -1,12 +0,0 @@ |
||||
package defaults |
||||
|
||||
// Setter is an interface for setting default values
|
||||
type Setter interface { |
||||
SetDefaults() |
||||
} |
||||
|
||||
func callSetter(v interface{}) { |
||||
if ds, ok := v.(Setter); ok { |
||||
ds.SetDefaults() |
||||
} |
||||
} |
Loading…
Reference in new issue