diff --git a/lib/msgraph/client.go b/lib/msgraph/client.go index 7543f39fb1155..081d6b28da239 100644 --- a/lib/msgraph/client.go +++ b/lib/msgraph/client.go @@ -37,6 +37,7 @@ import ( "github.com/google/uuid" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" + "github.com/prometheus/client_golang/prometheus" "github.com/gravitational/teleport" apidefaults "github.com/gravitational/teleport/api/defaults" @@ -98,6 +99,9 @@ type Config struct { // GraphEndpoint specifies root domain of the Graph API. GraphEndpoint string Logger *slog.Logger + // MetricsRegistry configures where metrics should be registered. + // When nil, metrics are created but not registered. + MetricsRegistry prometheus.Registerer } // SetDefaults sets the default values for optional fields. @@ -144,6 +148,7 @@ type Client struct { baseURL *url.URL pageSize int logger *slog.Logger + metrics *clientMetrics } // NewClient returns a new client for the given config. @@ -156,6 +161,15 @@ func NewClient(cfg Config) (*Client, error) { if err != nil { return nil, trace.Wrap(err) } + + metrics := newMetrics() + // gracefully handle not being given a metric registry + if cfg.MetricsRegistry != nil { + if err := metrics.register(cfg.MetricsRegistry); err != nil { + return nil, trace.Wrap(err, "registering metrics") + } + } + return &Client{ httpClient: cfg.HTTPClient, tokenProvider: cfg.TokenProvider, @@ -164,6 +178,7 @@ func NewClient(cfg Config) (*Client, error) { baseURL: base.JoinPath(graphVersion), pageSize: cfg.PageSize, logger: cfg.Logger, + metrics: metrics, }, nil } @@ -201,6 +216,7 @@ func (c *Client) request(ctx context.Context, method string, uri string, header } var lastErr error + var start time.Time for range maxRetries { if retryAfter > 0 { select { @@ -231,10 +247,13 @@ func (c *Client) request(ctx context.Context, method string, uri string, header // https://learn.microsoft.com/en-us/graph/best-practices-concept#reliability-and-support req.Header.Set("client-request-id", requestID) + start = c.clock.Now() resp, err := c.httpClient.Do(req) if err != nil { return nil, trace.Wrap(err) // hard I/O error, bail } + c.metrics.requestDuration.WithLabelValues(method).Observe(c.clock.Since(start).Seconds()) + c.metrics.requestTotal.WithLabelValues(method, strconv.Itoa(resp.StatusCode)) if resp.StatusCode >= 200 && resp.StatusCode < 400 { return resp, nil diff --git a/lib/msgraph/metrics.go b/lib/msgraph/metrics.go new file mode 100644 index 0000000000000..2c762ea17ff18 --- /dev/null +++ b/lib/msgraph/metrics.go @@ -0,0 +1,64 @@ +/* + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package msgraph + +import ( + "github.com/gravitational/trace" + "github.com/prometheus/client_golang/prometheus" + + "github.com/gravitational/teleport" +) + +type clientMetrics struct { + // requestsTotal keeps track of the number of requests done by the client + // This metric is labeled by status code. + requestTotal *prometheus.CounterVec + // requestDuration keeps track of the request duration, in seconds. + requestDuration *prometheus.HistogramVec +} + +const ( + metricSubsystem = "msgraph" + metricsLabelStatus = "status" + metricsLabelsMethod = "method" +) + +func newMetrics() *clientMetrics { + return &clientMetrics{ + requestTotal: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: teleport.MetricNamespace, + Subsystem: metricSubsystem, + Name: "request_total", + Help: "Total number of requests made to MS Graph", + }, []string{metricsLabelsMethod, metricsLabelStatus}), + requestDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: teleport.MetricNamespace, + Subsystem: metricSubsystem, + Name: "request_duration_seconds", + Help: "Request to MS Graph duration in seconds.", + }, []string{metricsLabelsMethod}), + } +} + +func (metrics *clientMetrics) register(r prometheus.Registerer) error { + return trace.NewAggregate( + r.Register(metrics.requestTotal), + r.Register(metrics.requestDuration), + ) +}