Files
opencloud/services/graph/pkg/identity/backend_prometheus.go
Pascal Bleser 5b72318493 chore(graph): add metrics for HTTP API and LDAP
Introducing gowrap as a build-time tool to generate interface delegate
structs from templates:

 * added as a 'make go-generate' target in services/graph,
 * added as a build-time dependency in .bingo/

Introduce an LDAP client abstraction interface to be able to wrap the
go-ldap client API with metrics transparently (and possibly hooks and
such in the future), in order to use delegation patterns to measure the
time LDAP (client) operations take to finish, as well as to track their
results (success, failure, not-found).

Has two implementations that are generated using gowrap:

* a go-ldap adapter implementation that directly delegates to a go-ldap
  connection
* a time measuring and metrics collecting implementation that delegates
  to another LdapClient

The metrics collecting one is disabled by default, can be enabled with
GRAPH_LDAP_METRICS_DISABLE=false

It collects durations of outbound LDAP client operations into a histogram, as
well as the number of concurrent outbound LDAP operations in a gauge (via an
atomic int and a gauge function, as that performs best).

Add an HTTP middleware that measures how long Graph HTTP API requests
take, storing taken time into a histogram along with labels for

 * method,
 * path pattern (from the chi routes),
 * Graph API version prefix,
 * Graph API resource name,
 * and the resulting status code.

It also tracks the number of concurrent inbound Graph API HTTP requests
using a gauge (also using an atomic int and a gauge function).

Disabled by default, can be enabled with
GRAPH_HTTP_METRICS_DISABLE=false

Add Backend and EducationBackend delegate implementations that measure
execution time on the level of the higher API call operations there
(CreateUser, DeleteUser, ..., CreateSchool, ...), generated using
gowrap.

Disabled by default, can be enabled with
GRAPH_IDENTITY_BACKEND_METRICS_DISABLE=false

Also added a small k6 script to produce some read-only load on the Graph
API, for a casual test of the metrics, as well as k6 in mise.toml.

Make an internal changes to how singular LDAP entry searches work in the LDAP
identity backends:

 * check whether searches for a singular entry returns more than one
   result, in which case a new error TooManyResults is returned, instead
   of leaving that undetected, blindly taking the first result, and
   potentially risking data inconsistencies

Improve the loggers in identity backends by adding attributes for their
request targets (Reva gateway address or LDAP URI, respectively).

Also add a "backend" attribute for all Graph API logs (set to "ldap" or
"cs3"), to help debug potential issues, and remove them from all the logger
debug calls at the beginning of each LDAP-related function as those should
really be part of the logger and set beforehand.

The LDAP identity backend logger also has two new attributes to help
debugging with logs:

 * write (bool): whether write operations are enabled
 * refint (bool): whether refint is enabled or not

Also adds a dedicated counter metric for user password change operations.

Minor campfire improvements:

 * add a constructor func for the CS3 backend

 * add a constructor func for the LDAP backend

 * in the LDAP identity backend, in searchLDAPEntryByFilter (used by all
   search/get public functions), errors that occur when performing LDAP
   SEARCH operations were blindly mapped to a ItemNotFound error,
   instead of being analyzed as it could be caused by a technical error

 * in the requireadmin middleware, add debug logging to explain why a
   request is denied

 * when an LDAP password change fails because the user entry was not
   found in LDAP, we now have a log message that tracks that
2026-09-01 10:57:47 +02:00

501 lines
13 KiB
Go

// Code generated by gowrap. DO NOT EDIT.
// template: backend_prometheus.tmpl
// gowrap: http://github.com/hexdigest/gowrap
package identity
import (
"context"
"net/url"
"time"
"github.com/CiscoM31/godata"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
"github.com/prometheus/client_golang/prometheus"
)
// PrometheusBackend implements Backend interface with all methods wrapped
// with Prometheus metrics
type PrometheusBackend struct {
delegate Backend
metric *prometheus.HistogramVec
}
var _ Backend = &PrometheusBackend{}
// returns an instance of the Backend decorated with prometheus metric
func NewPrometheusBackend(delegate Backend, metric *prometheus.HistogramVec) PrometheusBackend {
return PrometheusBackend{
delegate: delegate,
metric: metric,
}
}
// AddMembersToGroup implements Backend.AddMembersToGroup
func (_d PrometheusBackend) AddMembersToGroup(ctx context.Context, groupID string, memberID []string) (err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpAddMembersToGroup, result).Observe(duration)
}()
return _d.delegate.AddMembersToGroup(ctx, groupID, memberID)
}
// CreateGroup implements Backend.CreateGroup
func (_d PrometheusBackend) CreateGroup(ctx context.Context, group libregraph.Group) (gp1 *libregraph.Group, err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpCreateGroup, result).Observe(duration)
}()
return _d.delegate.CreateGroup(ctx, group)
}
// CreateUser implements Backend.CreateUser
func (_d PrometheusBackend) CreateUser(ctx context.Context, user libregraph.User) (up1 *libregraph.User, err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpCreateUser, result).Observe(duration)
}()
return _d.delegate.CreateUser(ctx, user)
}
// DeleteGroup implements Backend.DeleteGroup
func (_d PrometheusBackend) DeleteGroup(ctx context.Context, id string) (err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpDeleteGroup, result).Observe(duration)
}()
return _d.delegate.DeleteGroup(ctx, id)
}
// DeleteUser implements Backend.DeleteUser
func (_d PrometheusBackend) DeleteUser(ctx context.Context, nameOrID string) (err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpDeleteUser, result).Observe(duration)
}()
return _d.delegate.DeleteUser(ctx, nameOrID)
}
// FilterUsers implements Backend.FilterUsers
func (_d PrometheusBackend) FilterUsers(ctx context.Context, oreq *godata.GoDataRequest, filter *godata.ParseNode) (upa1 []*libregraph.User, err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpFilterUsers, result).Observe(duration)
}()
return _d.delegate.FilterUsers(ctx, oreq, filter)
}
// GetGroup implements Backend.GetGroup
func (_d PrometheusBackend) GetGroup(ctx context.Context, nameOrID string, queryParam url.Values) (gp1 *libregraph.Group, err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
// it's a get operation that returns a pointer (and not an array): check whether that's nil or not
if gp1 == nil {
result = MetricResultNotFound
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpGetGroup, result).Observe(duration)
}()
return _d.delegate.GetGroup(ctx, nameOrID, queryParam)
}
// GetGroupMembers implements Backend.GetGroupMembers
func (_d PrometheusBackend) GetGroupMembers(ctx context.Context, id string, oreq *godata.GoDataRequest) (upa1 []*libregraph.User, err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpGetGroupMembers, result).Observe(duration)
}()
return _d.delegate.GetGroupMembers(ctx, id, oreq)
}
// GetGroups implements Backend.GetGroups
func (_d PrometheusBackend) GetGroups(ctx context.Context, oreq *godata.GoDataRequest) (gpa1 []*libregraph.Group, err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpGetGroups, result).Observe(duration)
}()
return _d.delegate.GetGroups(ctx, oreq)
}
// GetUser implements Backend.GetUser
func (_d PrometheusBackend) GetUser(ctx context.Context, nameOrID string, oreq *godata.GoDataRequest) (up1 *libregraph.User, err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
// it's a get operation that returns a pointer (and not an array): check whether that's nil or not
if up1 == nil {
result = MetricResultNotFound
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpGetUser, result).Observe(duration)
}()
return _d.delegate.GetUser(ctx, nameOrID, oreq)
}
// GetUsers implements Backend.GetUsers
func (_d PrometheusBackend) GetUsers(ctx context.Context, oreq *godata.GoDataRequest) (upa1 []*libregraph.User, err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpGetUsers, result).Observe(duration)
}()
return _d.delegate.GetUsers(ctx, oreq)
}
// RemoveMemberFromGroup implements Backend.RemoveMemberFromGroup
func (_d PrometheusBackend) RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) (err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpRemoveMemberFromGroup, result).Observe(duration)
}()
return _d.delegate.RemoveMemberFromGroup(ctx, groupID, memberID)
}
// UpdateGroupName implements Backend.UpdateGroupName
func (_d PrometheusBackend) UpdateGroupName(ctx context.Context, groupID string, groupName string) (err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpUpdateGroupName, result).Observe(duration)
}()
return _d.delegate.UpdateGroupName(ctx, groupID, groupName)
}
// UpdateLastSignInDate implements Backend.UpdateLastSignInDate
func (_d PrometheusBackend) UpdateLastSignInDate(ctx context.Context, userID string, timestamp time.Time) (err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpUpdateLastSignInDate, result).Observe(duration)
}()
return _d.delegate.UpdateLastSignInDate(ctx, userID, timestamp)
}
// UpdateUser implements Backend.UpdateUser
func (_d PrometheusBackend) UpdateUser(ctx context.Context, nameOrID string, user libregraph.UserUpdate) (up1 *libregraph.User, err error) {
_since := time.Now()
defer func() {
duration := time.Since(_since).Seconds()
result := MetricResultSuccess
if err != nil {
result = MetricResultFailure
if err == ErrReadOnly {
result = MetricResultReadOnly
}
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
} else {
if e, ok := errorcode.ToError(err); ok {
if e.GetCode() == errorcode.ItemNotFound {
result = MetricResultNotFound
}
}
}
_d.metric.WithLabelValues(MetricOpUpdateUser, result).Observe(duration)
}()
return _d.delegate.UpdateUser(ctx, nameOrID, user)
}