mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:17:32 +00:00
chore: move the go code into a subfolder
This commit is contained in:
37
apps/api/internal/telemetry/metrics/config/config.go
Normal file
37
apps/api/internal/telemetry/metrics/config/config.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/telemetry/metrics"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/metrics/otel"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Type string
|
||||
Config map[string]interface{} `mapstructure:",remain"`
|
||||
}
|
||||
|
||||
var meter = map[string]func(map[string]interface{}) error{
|
||||
"otel": otel.NewTracerFromConfig,
|
||||
"none": registerNoopMetrics,
|
||||
"": registerNoopMetrics,
|
||||
}
|
||||
|
||||
func (c *Config) NewMeter() error {
|
||||
// When using start-from-init or start-from-setup the metric provider
|
||||
// was already set in the setup phase and the start phase must not overwrite it.
|
||||
if metrics.M != nil {
|
||||
return nil
|
||||
}
|
||||
t, ok := meter[c.Type]
|
||||
if !ok {
|
||||
return zerrors.ThrowInternalf(nil, "METER-Dfqsx", "config type %s not supported", c.Type)
|
||||
}
|
||||
|
||||
return t(c.Config)
|
||||
}
|
||||
|
||||
func registerNoopMetrics(rawConfig map[string]interface{}) (err error) {
|
||||
metrics.M = &metrics.NoopMetrics{}
|
||||
return nil
|
||||
}
|
147
apps/api/internal/telemetry/metrics/http_handler.go
Normal file
147
apps/api/internal/telemetry/metrics/http_handler.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
const (
|
||||
RequestCounter = "http.server.request_count"
|
||||
RequestCountDescription = "Request counter"
|
||||
TotalRequestCounter = "http.server.total_request_count"
|
||||
TotalRequestDescription = "Total return code counter"
|
||||
ReturnCodeCounter = "http.server.return_code_counter"
|
||||
ReturnCodeCounterDescription = "Return code counter"
|
||||
Method = "method"
|
||||
URI = "uri"
|
||||
ReturnCode = "return_code"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
handler http.Handler
|
||||
methods []MetricType
|
||||
filters []Filter
|
||||
}
|
||||
|
||||
type MetricType int32
|
||||
|
||||
const (
|
||||
MetricTypeTotalCount MetricType = iota
|
||||
MetricTypeStatusCode
|
||||
MetricTypeRequestCount
|
||||
)
|
||||
|
||||
type StatusRecorder struct {
|
||||
http.ResponseWriter
|
||||
RequestURI *string
|
||||
Status int
|
||||
}
|
||||
|
||||
func (r *StatusRecorder) WriteHeader(status int) {
|
||||
r.Status = status
|
||||
r.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
type Filter func(*http.Request) bool
|
||||
|
||||
func NewMetricsHandler(handler http.Handler, metricMethods []MetricType, ignoredEndpoints ...string) http.Handler {
|
||||
h := Handler{
|
||||
handler: handler,
|
||||
methods: metricMethods,
|
||||
}
|
||||
if len(ignoredEndpoints) > 0 {
|
||||
h.filters = append(h.filters, shouldNotIgnore(ignoredEndpoints...))
|
||||
}
|
||||
return &h
|
||||
}
|
||||
|
||||
type key int
|
||||
|
||||
const requestURI key = iota
|
||||
|
||||
func SetRequestURIPattern(ctx context.Context, pattern string) {
|
||||
uri, ok := ctx.Value(requestURI).(*string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
*uri = pattern
|
||||
}
|
||||
|
||||
// ServeHTTP serves HTTP requests (http.Handler)
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if len(h.methods) == 0 {
|
||||
h.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
for _, f := range h.filters {
|
||||
if !f(r) {
|
||||
// Simply pass through to the handler if a filter rejects the request
|
||||
h.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
uri := strings.Split(r.RequestURI, "?")[0]
|
||||
recorder := &StatusRecorder{
|
||||
ResponseWriter: w,
|
||||
RequestURI: &uri,
|
||||
Status: 200,
|
||||
}
|
||||
r = r.WithContext(context.WithValue(r.Context(), requestURI, &uri))
|
||||
h.handler.ServeHTTP(recorder, r)
|
||||
if h.containsMetricsMethod(MetricTypeRequestCount) {
|
||||
RegisterRequestCounter(recorder, r)
|
||||
}
|
||||
if h.containsMetricsMethod(MetricTypeTotalCount) {
|
||||
RegisterTotalRequestCounter(r)
|
||||
}
|
||||
if h.containsMetricsMethod(MetricTypeStatusCode) {
|
||||
RegisterRequestCodeCounter(recorder, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) containsMetricsMethod(method MetricType) bool {
|
||||
for _, m := range h.methods {
|
||||
if m == method {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RegisterRequestCounter(recorder *StatusRecorder, r *http.Request) {
|
||||
var labels = map[string]attribute.Value{
|
||||
URI: attribute.StringValue(*recorder.RequestURI),
|
||||
Method: attribute.StringValue(r.Method),
|
||||
}
|
||||
RegisterCounter(RequestCounter, RequestCountDescription)
|
||||
AddCount(r.Context(), RequestCounter, 1, labels)
|
||||
}
|
||||
|
||||
func RegisterTotalRequestCounter(r *http.Request) {
|
||||
RegisterCounter(TotalRequestCounter, TotalRequestDescription)
|
||||
AddCount(r.Context(), TotalRequestCounter, 1, nil)
|
||||
}
|
||||
|
||||
func RegisterRequestCodeCounter(recorder *StatusRecorder, r *http.Request) {
|
||||
var labels = map[string]attribute.Value{
|
||||
URI: attribute.StringValue(*recorder.RequestURI),
|
||||
Method: attribute.StringValue(r.Method),
|
||||
ReturnCode: attribute.IntValue(recorder.Status),
|
||||
}
|
||||
RegisterCounter(ReturnCodeCounter, ReturnCodeCounterDescription)
|
||||
AddCount(r.Context(), ReturnCodeCounter, 1, labels)
|
||||
}
|
||||
|
||||
func shouldNotIgnore(endpoints ...string) func(r *http.Request) bool {
|
||||
return func(r *http.Request) bool {
|
||||
for _, endpoint := range endpoints {
|
||||
if strings.HasPrefix(r.URL.RequestURI(), endpoint) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
87
apps/api/internal/telemetry/metrics/metrics.go
Normal file
87
apps/api/internal/telemetry/metrics/metrics.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
const (
|
||||
ActiveSessionCounter = "zitadel.active_session_counter"
|
||||
ActiveSessionCounterDescription = "Active session counter"
|
||||
SpoolerDivCounter = "zitadel.spooler_div_milliseconds"
|
||||
SpoolerDivCounterDescription = "Spooler div from last successful run to now in milliseconds"
|
||||
Database = "database"
|
||||
ViewName = "view_name"
|
||||
)
|
||||
|
||||
type Metrics interface {
|
||||
GetExporter() http.Handler
|
||||
GetMetricsProvider() metric.MeterProvider
|
||||
RegisterCounter(name, description string) error
|
||||
AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error
|
||||
AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error
|
||||
RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error
|
||||
RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error
|
||||
RegisterHistogram(name, description, unit string, buckets []float64) error
|
||||
}
|
||||
|
||||
var M Metrics
|
||||
|
||||
func GetExporter() http.Handler {
|
||||
if M == nil {
|
||||
return nil
|
||||
}
|
||||
return M.GetExporter()
|
||||
}
|
||||
|
||||
func GetMetricsProvider() metric.MeterProvider {
|
||||
if M == nil {
|
||||
return nil
|
||||
}
|
||||
return M.GetMetricsProvider()
|
||||
}
|
||||
|
||||
func RegisterCounter(name, description string) error {
|
||||
if M == nil {
|
||||
return nil
|
||||
}
|
||||
return M.RegisterCounter(name, description)
|
||||
}
|
||||
|
||||
func AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error {
|
||||
if M == nil {
|
||||
return nil
|
||||
}
|
||||
return M.AddCount(ctx, name, value, labels)
|
||||
}
|
||||
|
||||
func AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error {
|
||||
if M == nil {
|
||||
return nil
|
||||
}
|
||||
return M.AddHistogramMeasurement(ctx, name, value, labels)
|
||||
}
|
||||
|
||||
func RegisterHistogram(name, description, unit string, buckets []float64) error {
|
||||
if M == nil {
|
||||
return nil
|
||||
}
|
||||
return M.RegisterHistogram(name, description, unit, buckets)
|
||||
}
|
||||
|
||||
func RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error {
|
||||
if M == nil {
|
||||
return nil
|
||||
}
|
||||
return M.RegisterUpDownSumObserver(name, description, callbackFunc)
|
||||
}
|
||||
|
||||
func RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error {
|
||||
if M == nil {
|
||||
return nil
|
||||
}
|
||||
return M.RegisterValueObserver(name, description, callbackFunc)
|
||||
}
|
95
apps/api/internal/telemetry/metrics/mock.go
Normal file
95
apps/api/internal/telemetry/metrics/mock.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
// MockMetrics implements the metrics.Metrics interface for testing
|
||||
type MockMetrics struct {
|
||||
mu sync.RWMutex
|
||||
histogramValues map[string][]float64
|
||||
counterValues map[string]int64
|
||||
histogramLabels map[string][]map[string]attribute.Value
|
||||
counterLabels map[string][]map[string]attribute.Value
|
||||
}
|
||||
|
||||
var _ Metrics = new(MockMetrics)
|
||||
|
||||
// NewMockMetrics creates a new Metrics instance for testing
|
||||
func NewMockMetrics() *MockMetrics {
|
||||
return &MockMetrics{
|
||||
histogramValues: make(map[string][]float64),
|
||||
counterValues: make(map[string]int64),
|
||||
histogramLabels: make(map[string][]map[string]attribute.Value),
|
||||
counterLabels: make(map[string][]map[string]attribute.Value),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockMetrics) GetExporter() http.Handler {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMetrics) GetMetricsProvider() metric.MeterProvider {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMetrics) RegisterCounter(name, description string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMetrics) AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.counterValues[name] += value
|
||||
m.counterLabels[name] = append(m.counterLabels[name], labels)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMetrics) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.histogramValues[name] = append(m.histogramValues[name], value)
|
||||
m.histogramLabels[name] = append(m.histogramLabels[name], labels)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMetrics) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMetrics) RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMetrics) RegisterHistogram(name, description, unit string, buckets []float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockMetrics) GetHistogramValues(name string) []float64 {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.histogramValues[name]
|
||||
}
|
||||
|
||||
func (m *MockMetrics) GetHistogramLabels(name string) []map[string]attribute.Value {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.histogramLabels[name]
|
||||
}
|
||||
|
||||
func (m *MockMetrics) GetCounterValue(name string) int64 {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.counterValues[name]
|
||||
}
|
||||
|
||||
func (m *MockMetrics) GetCounterLabels(name string) []map[string]attribute.Value {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.counterLabels[name]
|
||||
}
|
45
apps/api/internal/telemetry/metrics/noop.go
Normal file
45
apps/api/internal/telemetry/metrics/noop.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
type NoopMetrics struct{}
|
||||
|
||||
var _ Metrics = new(NoopMetrics)
|
||||
|
||||
func (n *NoopMetrics) GetExporter() http.Handler {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NoopMetrics) GetMetricsProvider() metric.MeterProvider {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NoopMetrics) RegisterCounter(name, description string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NoopMetrics) AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NoopMetrics) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NoopMetrics) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NoopMetrics) RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NoopMetrics) RegisterHistogram(name, description, unit string, buckets []float64) error {
|
||||
return nil
|
||||
}
|
20
apps/api/internal/telemetry/metrics/otel/config.go
Normal file
20
apps/api/internal/telemetry/metrics/otel/config.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package otel
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/telemetry/metrics"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MeterName string
|
||||
}
|
||||
|
||||
func NewTracerFromConfig(rawConfig map[string]interface{}) (err error) {
|
||||
c := new(Config)
|
||||
c.MeterName, _ = rawConfig["metername"].(string)
|
||||
return c.NewMetrics()
|
||||
}
|
||||
|
||||
func (c *Config) NewMetrics() (err error) {
|
||||
metrics.M, err = NewMetrics(c.MeterName)
|
||||
return err
|
||||
}
|
163
apps/api/internal/telemetry/metrics/otel/open_telemetry.go
Normal file
163
apps/api/internal/telemetry/metrics/otel/open_telemetry.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package otel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/exporters/prometheus"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||
sdk_metric "go.opentelemetry.io/otel/sdk/metric"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/telemetry/metrics"
|
||||
otel_resource "github.com/zitadel/zitadel/internal/telemetry/otel"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
Provider metric.MeterProvider
|
||||
Meter metric.Meter
|
||||
Counters sync.Map
|
||||
UpDownSumObserver sync.Map
|
||||
ValueObservers sync.Map
|
||||
Histograms sync.Map
|
||||
}
|
||||
|
||||
func NewMetrics(meterName string) (metrics.Metrics, error) {
|
||||
resource, err := otel_resource.ResourceWithService("ZITADEL")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exporter, err := prometheus.New()
|
||||
if err != nil {
|
||||
return &Metrics{}, err
|
||||
}
|
||||
// create a view to filter out unwanted attributes
|
||||
view := sdk_metric.NewView(
|
||||
sdk_metric.Instrument{
|
||||
Scope: instrumentation.Scope{Name: otelhttp.ScopeName},
|
||||
},
|
||||
sdk_metric.Stream{
|
||||
AttributeFilter: attribute.NewAllowKeysFilter("http.method", "http.status_code", "http.target"),
|
||||
},
|
||||
)
|
||||
meterProvider := sdk_metric.NewMeterProvider(
|
||||
sdk_metric.WithReader(exporter),
|
||||
sdk_metric.WithResource(resource),
|
||||
sdk_metric.WithView(view),
|
||||
)
|
||||
return &Metrics{
|
||||
Provider: meterProvider,
|
||||
Meter: meterProvider.Meter(meterName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Metrics) GetExporter() http.Handler {
|
||||
return promhttp.Handler()
|
||||
}
|
||||
|
||||
func (m *Metrics) GetMetricsProvider() metric.MeterProvider {
|
||||
return m.Provider
|
||||
}
|
||||
|
||||
func (m *Metrics) RegisterCounter(name, description string) error {
|
||||
if _, exists := m.Counters.Load(name); exists {
|
||||
return nil
|
||||
}
|
||||
counter, err := m.Meter.Int64Counter(name, metric.WithDescription(description))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Counters.Store(name, counter)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metrics) AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error {
|
||||
counter, exists := m.Counters.Load(name)
|
||||
if !exists {
|
||||
return zerrors.ThrowNotFound(nil, "METER-4u8fs", "Errors.Metrics.Counter.NotFound")
|
||||
}
|
||||
counter.(metric.Int64Counter).Add(ctx, value, MapToAddOption(labels)...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metrics) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error {
|
||||
histogram, exists := m.Histograms.Load(name)
|
||||
if !exists {
|
||||
return zerrors.ThrowNotFound(nil, "METER-5wwb1", "Errors.Metrics.Histogram.NotFound")
|
||||
}
|
||||
histogram.(metric.Float64Histogram).Record(ctx, value, MapToRecordOption(labels)...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metrics) RegisterHistogram(name, description, unit string, buckets []float64) error {
|
||||
if _, exists := m.Histograms.Load(name); exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
histogram, err := m.Meter.Float64Histogram(name,
|
||||
metric.WithDescription(description),
|
||||
metric.WithUnit(unit),
|
||||
metric.WithExplicitBucketBoundaries(buckets...),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Histograms.Store(name, histogram)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metrics) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error {
|
||||
if _, exists := m.UpDownSumObserver.Load(name); exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
counter, err := m.Meter.Int64ObservableUpDownCounter(name, metric.WithInt64Callback(callbackFunc), metric.WithDescription(description))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.UpDownSumObserver.Store(name, counter)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Metrics) RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error {
|
||||
if _, exists := m.UpDownSumObserver.Load(name); exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
gauge, err := m.Meter.Int64ObservableGauge(name, metric.WithInt64Callback(callbackFunc), metric.WithDescription(description))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.UpDownSumObserver.Store(name, gauge)
|
||||
return nil
|
||||
}
|
||||
|
||||
func MapToAddOption(labels map[string]attribute.Value) []metric.AddOption {
|
||||
return []metric.AddOption{metric.WithAttributes(labelsToAttributes(labels)...)}
|
||||
}
|
||||
|
||||
func MapToRecordOption(labels map[string]attribute.Value) []metric.RecordOption {
|
||||
return []metric.RecordOption{metric.WithAttributes(labelsToAttributes(labels)...)}
|
||||
}
|
||||
|
||||
func labelsToAttributes(labels map[string]attribute.Value) []attribute.KeyValue {
|
||||
if labels == nil {
|
||||
return nil
|
||||
}
|
||||
attributes := make([]attribute.KeyValue, 0, len(labels))
|
||||
for key, value := range labels {
|
||||
attributes = append(attributes, attribute.KeyValue{
|
||||
Key: attribute.Key(key),
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
return attributes
|
||||
}
|
Reference in New Issue
Block a user