snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:17:21 +05:00
commit b352a9a30d
472 changed files with 108375 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
// Proxy metrics - exported for use by proxy package.
//
// ProxyCallDurationSummary holds the legacy Summary instrument by default;
// when prometheus.native_histograms is enabled it is a no-op observer (see
// internal noopObserverVec), so callers can continue to construct cached
// observers and call .Observe() without nil-checks. ProxyCallDurationHistogram
// is the canonical companion — prefer it for new queries and OTel export.
var (
ProxyCallDurationSummary prometheus.ObserverVec
ProxyCallDurationHistogram *prometheus.HistogramVec
ProxyCallErrorCount *prometheus.CounterVec
ProxyCallInflightRequests *prometheus.GaugeVec
)
// API metrics - exported for use by api package.
//
// APICommandDurationSummary follows the same pattern as ProxyCallDurationSummary
// (real Summary by default, no-op when native_histograms is enabled).
// APICommandDurationHistogram is the canonical companion.
//
// RPCDurationSummary follows the same pattern. RPCDurationHistogram is the
// companion Histogram exposed as "rpc_duration_seconds_histogram" —
// unconditional, with native (sparse, exponential) schema when
// native_histograms is enabled.
var (
APICommandErrorsTotal *prometheus.CounterVec
APICommandDurationSummary prometheus.ObserverVec
APICommandDurationHistogram *prometheus.HistogramVec
RPCDurationSummary prometheus.ObserverVec
RPCDurationHistogram *prometheus.HistogramVec
)
// Consumer metrics - exported for use by consuming package
var (
ConsumerProcessedTotal *prometheus.CounterVec
ConsumerErrorsTotal *prometheus.CounterVec
)
// Shared poll proxy metrics - exported for use by proxy package
var (
SharedPollProxyRequestItems *prometheus.HistogramVec
SharedPollProxyResponseItems *prometheus.HistogramVec
)
// Middleware metrics - exported for use by middleware package
var (
ConnLimitReached prometheus.Counter
HTTPRequestsTotal *prometheus.CounterVec
)
// Postgres broker metrics exported for use by pgmapbroker and pgstreambroker.
//
// All carry a postgres_ name prefix: these are Postgres-specific (outbox,
// partitions, row cleanup), not generic broker concepts. Metrics emitted by both
// PG brokers are split per kind (broker_* for stream, map_broker_* for map) so
// the two never share a series. Cleanup is stream-only today.
var (
BrokerPostgresCleanupRemovedTotal *prometheus.CounterVec
BrokerPostgresOutboxCursorLagSeconds *prometheus.GaugeVec
MapBrokerPostgresOutboxCursorLagSeconds *prometheus.GaugeVec
BrokerPostgresPartitions *prometheus.GaugeVec
MapBrokerPostgresPartitions *prometheus.GaugeVec
)

View File

@@ -0,0 +1,145 @@
package graphite
import (
"context"
"fmt"
"io"
"net"
"regexp"
"strings"
"sync"
"time"
"github.com/FZambia/eagle"
"github.com/prometheus/client_golang/prometheus"
)
var re = regexp.MustCompile("[[:^ascii:]]")
// PreparePathComponent cleans string to be used as Graphite metric path.
func PreparePathComponent(s string) string {
s = re.ReplaceAllLiteralString(s, "_")
return strings.ReplaceAll(s, ".", "_")
}
// Exporter to Graphite.
type Exporter struct {
prefix string
address string
timeout time.Duration
tags bool
closeOnce sync.Once
closeCh chan struct{}
sink chan eagle.Metrics
eagle *eagle.Eagle
}
// Config for Graphite Exporter.
type Config struct {
Address string
Gatherer prometheus.Gatherer
Interval time.Duration
Prefix string
Tags bool
}
// New creates new Graphite Exporter.
func New(c Config) *Exporter {
exporter := &Exporter{
prefix: c.Prefix,
address: c.Address,
timeout: time.Second,
tags: c.Tags,
closeCh: make(chan struct{}),
sink: make(chan eagle.Metrics),
}
exporter.eagle = eagle.New(eagle.Config{
Gatherer: c.Gatherer,
Interval: c.Interval,
Sink: exporter.sink,
})
return exporter
}
func (e *Exporter) Run(ctx context.Context) error {
defer func() { _ = e.close() }()
for {
select {
case <-ctx.Done():
return ctx.Err()
case metrics := <-e.sink:
_ = e.exportOnce(metrics)
}
}
}
// Close stops exporter.
func (e *Exporter) close() error {
e.closeOnce.Do(func() {
close(e.closeCh)
_ = e.eagle.Close()
})
return nil
}
func (e *Exporter) exportOnce(metrics eagle.Metrics) error {
conn, err := net.DialTimeout("tcp", e.address, e.timeout)
if err != nil {
return err
}
defer func() { _ = conn.Close() }()
_ = e.write(conn, metrics)
return nil
}
func makeTags(labels []string) string {
if len(labels) == 0 {
return ""
}
var tagParts []string
for i := 0; i < len(labels); i += 2 {
tagParts = append(tagParts, fmt.Sprintf("%s=%s", labels[i], labels[i+1]))
}
return ";" + strings.Join(tagParts, ";")
}
func (e *Exporter) write(w io.Writer, metrics eagle.Metrics) error {
now := time.Now().Unix()
for _, item := range metrics.Items {
for _, metricValue := range item.Values {
parts := []string{e.prefix}
if item.Namespace != "" {
parts = append(parts, item.Namespace)
}
if item.Subsystem != "" {
parts = append(parts, item.Subsystem)
}
if item.Name != "" {
parts = append(parts, item.Name)
}
if metricValue.Name != "" {
parts = append(parts, metricValue.Name)
}
if !e.tags {
parts = append(parts, metricValue.Labels...)
}
key := strings.Join(parts, ".")
if e.tags {
key += makeTags(metricValue.Labels)
}
var err error
if item.Type == eagle.MetricTypeCounter {
_, err = fmt.Fprintf(w, "%s %d %d\n", key, int64(metricValue.Value), now)
} else {
_, err = fmt.Fprintf(w, "%s %f %d\n", key, metricValue.Value, now)
}
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,22 @@
package graphite
import (
"testing"
)
func TestPreparePathComponent(t *testing.T) {
testCases := []struct {
in, out string
}{
{in: "service", out: "service"},
{in: "service.local", out: "service_local"},
{in: "service.prod.", out: "service_prod_"},
{in: "приvет", out: "___v__"},
}
for _, tc := range testCases {
if want, got := tc.out, PreparePathComponent(tc.in); want != got {
t.Fatalf("error, got sanitized string %s, want %s", got, want)
}
}
}

View File

@@ -0,0 +1,40 @@
package metrics
import (
"strconv"
"time"
)
// API metric helper functions - these were previously in the api package
// IncAPIError increments the API error counter with a numeric error code.
func IncAPIError(protocol string, method string, code uint32) {
APICommandErrorsTotal.WithLabelValues(protocol, method, strconv.FormatUint(uint64(code), 10)).Inc()
}
// IncAPIErrorStringCode increments the API error counter with a string error code.
func IncAPIErrorStringCode(protocol string, method string, code string) {
APICommandErrorsTotal.WithLabelValues(protocol, method, code).Inc()
}
// ObserveAPICommand observes the duration of an API command.
func ObserveAPICommand(started time.Time, protocol string, method string) {
duration := time.Since(started).Seconds()
APICommandDurationSummary.WithLabelValues(protocol, method).Observe(duration)
APICommandDurationHistogram.WithLabelValues(protocol, method).Observe(duration)
}
// ObserveRPC observes the duration of an RPC call.
func ObserveRPC(started time.Time, protocol string, method string) {
duration := time.Since(started).Seconds()
RPCDurationSummary.WithLabelValues(protocol, method).Observe(duration)
RPCDurationHistogram.WithLabelValues(protocol, method).Observe(duration)
}
// Consumer metric helper functions - these were previously in the consuming package
// InitConsumerMetrics initializes consumer metrics with zero values for the given consumer name.
func InitConsumerMetrics(consumerName string) {
ConsumerProcessedTotal.WithLabelValues(consumerName).Add(0)
ConsumerErrorsTotal.WithLabelValues(consumerName).Add(0)
}

400
internal/metrics/metrics.go Normal file
View File

@@ -0,0 +1,400 @@
package metrics
import (
"errors"
"time"
"github.com/prometheus/client_golang/prometheus"
)
const defaultMetricsNamespace = "centrifugo"
// Config contains metrics configuration.
type Config struct {
// Namespace is the prometheus namespace for all metrics. If empty, defaults to "centrifugo".
Namespace string
// ConstLabels are labels that will be added to all metrics as constant labels.
// These are useful for adding environment, region, or other deployment-specific labels.
ConstLabels map[string]string
// Registerer is the prometheus registerer to use. If nil, prometheus.DefaultRegisterer is used.
Registerer prometheus.Registerer
// NativeHistograms switches Histogram instruments to Prometheus native
// (sparse, exponential) schema with no explicit buckets exposed. Designed
// for OpenTelemetry export via the client_golang Prometheus bridge.
// Text-format scrapes lose _bucket series; use protobuf scrape format.
NativeHistograms bool
}
// nativeHistogramOpts returns opts unchanged when native is false. When true,
// it enables Prometheus native histogram schema with no explicit buckets —
// the metric exposes only _count, _sum, and the native histogram chunk.
func nativeHistogramOpts(opts prometheus.HistogramOpts, native bool) prometheus.HistogramOpts {
if !native {
return opts
}
opts.Buckets = nil
opts.NativeHistogramBucketFactor = 1.1
opts.NativeHistogramMaxBucketNumber = 200
opts.NativeHistogramMinResetDuration = time.Hour
return opts
}
// noopObserverVec implements prometheus.ObserverVec with all no-op methods.
// Assigned to Summary accessors when NativeHistograms is true so that
// callers (proxy handlers, helpers) can still construct cached observers
// and call .Observe() without nil checks — the calls become no-ops and no
// metric data is recorded for the disabled Summary instrument.
type noopObserverVec struct{}
func (noopObserverVec) Describe(chan<- *prometheus.Desc) {}
func (noopObserverVec) Collect(chan<- prometheus.Metric) {}
func (noopObserverVec) WithLabelValues(...string) prometheus.Observer { return noopObserver{} }
func (noopObserverVec) With(prometheus.Labels) prometheus.Observer { return noopObserver{} }
func (noopObserverVec) GetMetricWith(prometheus.Labels) (prometheus.Observer, error) {
return noopObserver{}, nil
}
func (noopObserverVec) GetMetricWithLabelValues(...string) (prometheus.Observer, error) {
return noopObserver{}, nil
}
func (noopObserverVec) CurryWith(prometheus.Labels) (prometheus.ObserverVec, error) {
return noopObserverVec{}, nil
}
func (noopObserverVec) MustCurryWith(prometheus.Labels) prometheus.ObserverVec {
return noopObserverVec{}
}
type noopObserver struct{}
func (noopObserver) Observe(float64) {}
// Registry holds all Centrifugo metrics.
type Registry struct {
config Config
// Proxy metrics
proxyCallDurationSummary prometheus.ObserverVec
proxyCallDurationHistogram *prometheus.HistogramVec
proxyCallErrorCount *prometheus.CounterVec
proxyCallInflightRequests *prometheus.GaugeVec
// API metrics
apiCommandErrorsTotal *prometheus.CounterVec
apiCommandDurationSummary prometheus.ObserverVec
apiCommandDurationHistogram *prometheus.HistogramVec
// rpcDurationSummary is the legacy Summary by default; no-op when
// NativeHistograms is true. rpcDurationHistogram is the new companion
// Histogram exposed as "rpc_duration_seconds_histogram" — unconditional,
// with native schema when NativeHistograms is true.
rpcDurationSummary prometheus.ObserverVec
rpcDurationHistogram *prometheus.HistogramVec
// Consumer metrics
consumerProcessedTotal *prometheus.CounterVec
consumerErrorsTotal *prometheus.CounterVec
// Shared poll proxy metrics
sharedPollProxyRequestItems *prometheus.HistogramVec
sharedPollProxyResponseItems *prometheus.HistogramVec
// Middleware metrics
connLimitReached prometheus.Counter
httpRequestsTotal *prometheus.CounterVec
// PostgreSQL-specific broker metrics. Cleanup is Postgres-specific here (only
// the PG stream broker prunes rows, with PG table names in the "pass" label),
// so it carries the postgres_ prefix like the rest. Metrics emitted by both
// PG brokers are split per kind so stream and map never share a series.
brokerPostgresCleanupRemovedTotal *prometheus.CounterVec
brokerPostgresOutboxCursorLagSeconds *prometheus.GaugeVec
mapBrokerPostgresOutboxCursorLagSeconds *prometheus.GaugeVec
brokerPostgresPartitions *prometheus.GaugeVec
mapBrokerPostgresPartitions *prometheus.GaugeVec
}
// Init initializes the metrics registry with the provided configuration.
// It creates all metrics and registers them with the provided registerer.
// If registerer is nil, prometheus.DefaultRegisterer is used.
// Returns an error if metric registration fails.
func Init(cfg Config) error {
reg, err := newRegistry(cfg)
if err != nil {
return err
}
// Populate exported variables for backward compatibility
ProxyCallDurationSummary = reg.proxyCallDurationSummary
ProxyCallDurationHistogram = reg.proxyCallDurationHistogram
ProxyCallErrorCount = reg.proxyCallErrorCount
ProxyCallInflightRequests = reg.proxyCallInflightRequests
APICommandErrorsTotal = reg.apiCommandErrorsTotal
APICommandDurationSummary = reg.apiCommandDurationSummary
APICommandDurationHistogram = reg.apiCommandDurationHistogram
RPCDurationSummary = reg.rpcDurationSummary
RPCDurationHistogram = reg.rpcDurationHistogram
ConsumerProcessedTotal = reg.consumerProcessedTotal
ConsumerErrorsTotal = reg.consumerErrorsTotal
SharedPollProxyRequestItems = reg.sharedPollProxyRequestItems
SharedPollProxyResponseItems = reg.sharedPollProxyResponseItems
ConnLimitReached = reg.connLimitReached
HTTPRequestsTotal = reg.httpRequestsTotal
BrokerPostgresCleanupRemovedTotal = reg.brokerPostgresCleanupRemovedTotal
BrokerPostgresOutboxCursorLagSeconds = reg.brokerPostgresOutboxCursorLagSeconds
MapBrokerPostgresOutboxCursorLagSeconds = reg.mapBrokerPostgresOutboxCursorLagSeconds
BrokerPostgresPartitions = reg.brokerPostgresPartitions
MapBrokerPostgresPartitions = reg.mapBrokerPostgresPartitions
return nil
}
func newRegistry(cfg Config) (*Registry, error) {
registerer := cfg.Registerer
if registerer == nil {
registerer = prometheus.DefaultRegisterer
}
metricsNamespace := cfg.Namespace
if metricsNamespace == "" {
metricsNamespace = defaultMetricsNamespace
}
constLabels := prometheus.Labels(cfg.ConstLabels)
m := &Registry{
config: cfg,
}
// Proxy metrics
if cfg.NativeHistograms {
m.proxyCallDurationSummary = noopObserverVec{}
} else {
m.proxyCallDurationSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{
Namespace: metricsNamespace,
Subsystem: "proxy",
Name: "duration_seconds",
Objectives: map[float64]float64{0.5: 0.05, 0.99: 0.001, 0.999: 0.0001},
Help: "DEPRECATED — use centrifugo_proxy_duration_seconds_histogram. Will be removed in Centrifugo v7. Duration of proxy call.",
ConstLabels: constLabels,
}, []string{"protocol", "type", "name"})
}
m.proxyCallDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "proxy",
Name: "duration_seconds_histogram",
Buckets: prometheus.DefBuckets,
Help: "Histogram of duration of proxy call.",
ConstLabels: constLabels,
}, cfg.NativeHistograms), []string{"protocol", "type", "name"})
m.proxyCallErrorCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "proxy",
Name: "errors",
Help: "Proxy call error count.",
ConstLabels: constLabels,
}, []string{"protocol", "type", "name"})
m.proxyCallInflightRequests = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "proxy",
Name: "inflight_requests",
Help: "Number of inflight proxy requests.",
ConstLabels: constLabels,
}, []string{"protocol", "type", "name"})
// API metrics
m.apiCommandErrorsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "api",
Name: "command_errors_total",
Help: "Total errors in API commands.",
ConstLabels: constLabels,
}, []string{"protocol", "method", "error"})
if cfg.NativeHistograms {
m.apiCommandDurationSummary = noopObserverVec{}
} else {
m.apiCommandDurationSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{
Namespace: metricsNamespace,
Subsystem: "api",
Name: "command_duration_seconds",
Objectives: map[float64]float64{0.5: 0.05, 0.99: 0.001, 0.999: 0.0001},
Help: "DEPRECATED — use centrifugo_api_command_duration_seconds_histogram. Will be removed in Centrifugo v7. Duration of API per command.",
ConstLabels: constLabels,
}, []string{"protocol", "method"})
}
m.apiCommandDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "api",
Buckets: prometheus.DefBuckets,
Name: "command_duration_seconds_histogram",
Help: "Histogram of duration of API per command.",
ConstLabels: constLabels,
}, cfg.NativeHistograms), []string{"protocol", "method"})
if cfg.NativeHistograms {
m.rpcDurationSummary = noopObserverVec{}
} else {
m.rpcDurationSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{
Namespace: metricsNamespace,
Subsystem: "api",
Name: "rpc_duration_seconds",
Objectives: map[float64]float64{0.5: 0.05, 0.99: 0.001, 0.999: 0.0001},
Help: "DEPRECATED — use centrifugo_api_rpc_duration_seconds_histogram. Will be removed in Centrifugo v7. Duration of API per command.",
ConstLabels: constLabels,
}, []string{"protocol", "method"})
}
m.rpcDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "api",
Name: "rpc_duration_seconds_histogram",
Buckets: prometheus.DefBuckets,
Help: "Histogram of duration of API per command.",
ConstLabels: constLabels,
}, cfg.NativeHistograms), []string{"protocol", "method"})
// Consumer metrics
m.consumerProcessedTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "consumers",
Name: "messages_processed_total",
Help: "Total number of processed messages in consumer",
ConstLabels: constLabels,
}, []string{"consumer_name"})
m.consumerErrorsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "consumers",
Name: "errors_total",
Help: "Total number of errors in consumer",
ConstLabels: constLabels,
}, []string{"consumer_name"})
// Shared poll proxy metrics
m.sharedPollProxyRequestItems = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll_proxy",
Name: "request_items",
Help: "Number of items per shared poll proxy request.",
Buckets: []float64{1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
ConstLabels: constLabels,
}, cfg.NativeHistograms), []string{"name"})
m.sharedPollProxyResponseItems = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: "shared_poll_proxy",
Name: "response_items",
Help: "Number of items per shared poll proxy response.",
Buckets: []float64{1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
ConstLabels: constLabels,
}, cfg.NativeHistograms), []string{"name"})
// Middleware metrics
m.connLimitReached = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "client_connection_limit",
Help: "Number of refused requests due to node client connection limit.",
ConstLabels: constLabels,
})
m.httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "node",
Name: "incoming_http_requests_total",
Help: "Number of incoming HTTP requests",
ConstLabels: constLabels,
},
[]string{"path", "method", "status"},
)
// Postgres-specific broker metrics carry a postgres_ name prefix. Cleanup is
// Postgres-specific here: only the PG stream broker prunes rows (the "pass"
// label is the PG table being cleaned), so it is not the generic cleanup
// metric. Metrics emitted by both PG brokers are split per kind below.
m.brokerPostgresCleanupRemovedTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: "broker",
Name: "postgres_cleanup_removed_total",
Help: "Total rows removed by each Postgres cleanup pass.",
ConstLabels: constLabels,
}, []string{"broker_name", "pass"})
m.brokerPostgresOutboxCursorLagSeconds = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "broker",
Name: "postgres_outbox_cursor_lag_seconds",
Help: "Time between the outbox cursor's row created_at and now, per shard.",
ConstLabels: constLabels,
}, []string{"broker_name", "shard"})
m.mapBrokerPostgresOutboxCursorLagSeconds = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "map_broker",
Name: "postgres_outbox_cursor_lag_seconds",
Help: "Time between the outbox cursor's row created_at and now, per shard.",
ConstLabels: constLabels,
}, []string{"broker_name", "shard"})
m.brokerPostgresPartitions = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "broker",
Name: "postgres_partitions",
Help: "Count of stream/history table partitions.",
ConstLabels: constLabels,
}, []string{"broker_name"})
m.mapBrokerPostgresPartitions = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: "map_broker",
Name: "postgres_partitions",
Help: "Count of stream/history table partitions.",
ConstLabels: constLabels,
}, []string{"broker_name"})
// Register all metrics
var alreadyRegistered prometheus.AlreadyRegisteredError
collectors := []prometheus.Collector{
m.proxyCallDurationSummary,
m.proxyCallDurationHistogram,
m.proxyCallErrorCount,
m.proxyCallInflightRequests,
m.apiCommandErrorsTotal,
m.apiCommandDurationSummary,
m.apiCommandDurationHistogram,
m.rpcDurationSummary,
m.rpcDurationHistogram,
m.consumerProcessedTotal,
m.consumerErrorsTotal,
m.sharedPollProxyRequestItems,
m.sharedPollProxyResponseItems,
m.connLimitReached,
m.httpRequestsTotal,
m.brokerPostgresCleanupRemovedTotal,
m.brokerPostgresOutboxCursorLagSeconds,
m.mapBrokerPostgresOutboxCursorLagSeconds,
m.brokerPostgresPartitions,
m.mapBrokerPostgresPartitions,
}
for _, collector := range collectors {
err := registerer.Register(collector)
if err != nil {
// Ignore if already registered (allows re-initialization in tests)
if !errors.As(err, &alreadyRegistered) {
return nil, err
}
}
}
return m, nil
}