diff --git a/.gitignore b/.gitignore index fc78bbc..8a72935 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -389ds_exporter .build -.tarballs bin -.vscode \ No newline at end of file +.vscode +config.* +.docker \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a873550..0000000 --- a/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM quay.io/prometheus/busybox:latest -LABEL maintainer="adis.veletanlic@gmail.com" - -COPY bin/exporter /bin/exporter - -EXPOSE 9496 -ENTRYPOINT [ "/bin/exporter" ] diff --git a/README.md b/README.md index 00a9fa3..a74500a 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,34 @@ sudo chown -R go-ldap-metrics-exporter:go-ldap-metrics-exporter /opt/go-ldap-met ``` The go-ldap-metrics-exporter service can then be started using the service file provided in the repository. The service file is named `go-ldap-metrics-exporter.service`. + +## Configuration + +The configuration file is a .json file, and must contain the following fields: + +```json +{ + "ldap": { + "address": "ldap://localhost", + "fullDn": "uid=,ou=,dc=local,dc=domain,dc=com", + "baseDn": "dc=local,dc=domain,dc=com", + "password": "admin" + }, + "scrape": { + "interval": 60 + }, + "server": { + "active": true, + "address": "localhost", + "port": "9496" + }, + "log": { + "level": "info", + "json": true + }, + "export": { + "file": "/home/adve/go-ldap-metrics-exporter/textfile_collector/metrics.prom", + "interval": 300 + } +} +``` \ No newline at end of file diff --git a/config.json b/config.json deleted file mode 100644 index e2e3d0d..0000000 --- a/config.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "ldap": { - "address": "ldap://localhost", // will use port 389 - "user": "service-user", - "password": "secret-password" - }, - "ipa": { - "domain": "some.domain.org" - }, - "scrape": { - "interval": 60 - }, - "server": { - "active": false, - "address": "localhost", - "port": "9496" - }, - "log": { - "level": "debug", - "json": true - }, - "export": { - "file": "/var/lib/node_exporter/textfile_collector/metrics.prom", - "interval": 300 - } -} \ No newline at end of file diff --git a/internal/pkg/common/format.go b/internal/pkg/common/format.go deleted file mode 100644 index 1b15c71..0000000 --- a/internal/pkg/common/format.go +++ /dev/null @@ -1,13 +0,0 @@ -package common - -import "strings" - -func IpaDomainToBaseDN(domain string) string { - return "dc=" + strings.Replace(domain, ".", ",dc=", -1) -} - -// function to use Ldap.User (for example "admin") -// and for example generate string "cn=admin,dc=example,dc=com" -func UserWithBaseDN(user string, domain string) string { - return "cn=" + user + "," + IpaDomainToBaseDN(domain) -} \ No newline at end of file diff --git a/internal/pkg/common/gauge.go b/internal/pkg/common/gauge.go deleted file mode 100644 index 32b9079..0000000 --- a/internal/pkg/common/gauge.go +++ /dev/null @@ -1,57 +0,0 @@ -package common - -import "github.com/prometheus/client_golang/prometheus" - -var ( - UsersGauge = newGaugeVec("users", "Number of user accounts", []string{"type"}) - GroupsGauge = newGaugeVec("groups", "Number of groups", nil) - HostsGauge = newGaugeVec("hosts", "Number of hosts", nil) - HostGroupsGauge = newGaugeVec("hostgroups", "Number of hostgroups", nil) - HbacRulesGauge = newGaugeVec("hbac_rules", "Number of hbac rules", nil) - SudoRulesGauge = newGaugeVec("sudo_rules", "Number of sudo rules", nil) - DnsZonesGauge = newGaugeVec("dns_zones", "Number of dns zones", nil) - LdapConflictsGauge = newGaugeVec("replication_conflicts", "Number of ldap conflicts", nil) - ReplicationStatusGauge = newGaugeVec("replication_status", "Replication status by server", []string{"server"}) - ScrapeCounter = newCounterVec("scrape_count", "successful vs unsuccessful ldap scrape attempts", []string{"result"}) - ScrapeDurationGauge = newGaugeVec("scrape_duration_seconds", "time taken per scrape", nil) - CurrentConnectionsGauge = newGaugeVec("current_connections", "Current number of connections to the LDAP server", nil) - TotalConnectionsGauge = newGaugeVec("total_connections", "Total number of connections to the LDAP server", nil) - EntriesGauge = newGaugeVec("entries", "Number of entries in the LDAP server", nil) - OperationsCompletedGauge = newGaugeVec("operations_completed", "Number of operations performed by the LDAP server", nil) - OperationsInitiatedGauge = newGaugeVec("operations_initiated", "Number of operations initiated by the LDAP server", nil) - ThreadsGauge = newGaugeVec("threads", "Number of threads in the LDAP server", nil) - BytesSentGauge = newGaugeVec("bytes_sent", "Number of bytes sent by the LDAP server", nil) - VersionGauge = newGaugeVec("version", "LDAP server version", nil) -) - -const ( - subsystem = "ldap_389ds" -) - -/** - * Create a new gauge metric with labels. - */ -func newGaugeVec(name, help string, labels []string) *prometheus.GaugeVec { - return prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Subsystem: subsystem, - Name: name, - Help: help, - }, - labels, - ) -} - -/** - * Create a new counter metric with labels. - */ -func newCounterVec(name, help string, labels []string) *prometheus.CounterVec { - return prometheus.NewCounterVec( - prometheus.CounterOpts{ - Subsystem: subsystem, - Name: name, - Help: help, - }, - labels, - ) -} diff --git a/internal/pkg/ldap/query.go b/internal/pkg/ldap/query.go deleted file mode 100644 index 9f3a9c7..0000000 --- a/internal/pkg/ldap/query.go +++ /dev/null @@ -1,118 +0,0 @@ -package ldap - -import ( - "fmt" - "go-ldap-metrics-exporter/internal/pkg/common" - "strconv" - "strings" - - log "github.com/sirupsen/logrus" - "gopkg.in/ldap.v2" -) - -var ldapEscaper = strings.NewReplacer("=", "\\=", ",", "\\,") - -/** - * Query the LDAP server for the number of subordinates under a given base DN. - */ -func SubordinateQuery(l *ldap.Conn, baseDN, searchFilter string) (float64, error) { - req := ldap.NewSearchRequest( - baseDN, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, - searchFilter, []string{"numSubordinates"}, nil, - ) - sr, err := l.Search(req) - if err != nil { - return -1, err - } - - if len(sr.Entries) == 0 { - return -1, fmt.Errorf("no entries contain numSubordinates for %s (%s)", baseDN, searchFilter) - } - - val := sr.Entries[0].GetAttributeValue("numSubordinates") - num, err := strconv.ParseFloat(val, 64) - if err != nil { - return -1, err - } - - return num, nil -} - -/** - * Query the LDAP server for the number of entries matching a given filter. - */ -func CountQuery(l *ldap.Conn, baseDN, searchFilter, attr string, scope int) (float64, error) { - req := ldap.NewSearchRequest( - baseDN, scope, ldap.NeverDerefAliases, 0, 0, false, - searchFilter, []string{attr}, nil, - ) - sr, err := l.Search(req) - if err != nil { - return -1, err - } - - num := float64(len(sr.Entries)) - - return num, nil -} - -/** - * Query the LDAP server for replication status. - */ -func ReplicationQuery(l *ldap.Conn, suffix string) error { - escaped_suffix := ldapEscaper.Replace(suffix) - base_dn := fmt.Sprintf("cn=replica,cn=%s,cn=mapping tree,cn=config", escaped_suffix) - - req := ldap.NewSearchRequest( - base_dn, ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false, - "(objectClass=nsds5replicationagreement)", []string{"nsDS5ReplicaHost", "nsds5replicaLastUpdateStatus"}, nil, - ) - sr, err := l.Search(req) - if err != nil { - return err - } - - for _, entry := range sr.Entries { - host := entry.GetAttributeValue("nsDS5ReplicaHost") - status := entry.GetAttributeValue("nsds5replicaLastUpdateStatus") - if strings.Contains(status, "Incremental update succeeded") { - common.ReplicationStatusGauge.WithLabelValues(host).Set(1) - } else if strings.Contains(status, "Problem connecting to replica") { - common.ReplicationStatusGauge.WithLabelValues(host).Set(0) - } else if strings.Contains(status, "Can't acquire busy replica") { - common.ReplicationStatusGauge.WithLabelValues(host).Set(1) - } else { - log.Warnf("Unknown replication status host: %s, status: %s", host, status) - common.ReplicationStatusGauge.WithLabelValues(host).Set(0) - } - } - - return nil -} - -/** - * Query the LDAP server for a specific monitor attribute. - */ -func MonitorAttributeQuery(l *ldap.Conn, baseDN, attribute string) (float64, error) { - req := ldap.NewSearchRequest( - baseDN, ldap.ScopeBaseObject, - ldap.NeverDerefAliases, 0, 0, false, - "(objectClass=*)", []string{attribute}, nil, - ) - sr, err := l.Search(req) - if err != nil { - return -1, err - } - - if len(sr.Entries) == 0 { - return -1, fmt.Errorf("no entries found for %s", baseDN) - } - - val := sr.Entries[0].GetAttributeValue(attribute) - num, err := strconv.ParseFloat(val, 64) - if err != nil { - return -1, err - } - - return num, nil -} diff --git a/internal/pkg/prometheus/init.go b/internal/pkg/prometheus/init.go deleted file mode 100644 index 20af841..0000000 --- a/internal/pkg/prometheus/init.go +++ /dev/null @@ -1,36 +0,0 @@ -package prometheus - -import ( - "go-ldap-metrics-exporter/internal/pkg/common" - - "github.com/prometheus/client_golang/prometheus" -) - -func Init() { - registerMetrics() -} - -/** - * Register all desired metrics with Prometheus. - */ -func registerMetrics() { - prometheus.MustRegister( - common.UsersGauge, - common.GroupsGauge, - common.HostsGauge, - common.HostGroupsGauge, - common.HbacRulesGauge, - common.SudoRulesGauge, - common.DnsZonesGauge, - common.LdapConflictsGauge, - common.ReplicationStatusGauge, - common.ScrapeCounter, - common.ScrapeDurationGauge, - common.CurrentConnectionsGauge, - common.TotalConnectionsGauge, - common.EntriesGauge, - common.OperationsCompletedGauge, - common.OperationsInitiatedGauge, - common.ThreadsGauge, - ) -} diff --git a/internal/pkg/prometheus/scrape.go b/internal/pkg/prometheus/scrape.go deleted file mode 100644 index 31cb623..0000000 --- a/internal/pkg/prometheus/scrape.go +++ /dev/null @@ -1,120 +0,0 @@ -package prometheus - -import ( - "fmt" - "go-ldap-metrics-exporter/internal/pkg/common" - "time" - - "go-ldap-metrics-exporter/internal/pkg/ldap" - - "github.com/hashicorp/go-multierror" - "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" - ext_ldap "gopkg.in/ldap.v2" -) - -type Metric struct { - Name string - QueryFunc func(*ext_ldap.Conn, string) (float64, error) - Gauge *prometheus.GaugeVec - LabelValue string -} - -/** - * Metrics to scrape from the LDAP server. - */ -var metrics = []Metric{ - {"people", func(l *ext_ldap.Conn, suffix string) (float64, error) { - return ldap.SubordinateQuery(l, fmt.Sprintf("ou=people,%s", suffix), "(objectClass=organizationalUnit)") - }, common.UsersGauge, "active"}, - - {"groups", func(l *ext_ldap.Conn, suffix string) (float64, error) { - return ldap.SubordinateQuery(l, fmt.Sprintf("ou=groups,%s", suffix), "(objectClass=organizationalUnit)") - }, common.GroupsGauge, ""}, - - // LDAP server metrics queries, requires access to cn=monitor - {"current_connections", func(l *ext_ldap.Conn, suffix string) (float64, error) { - return ldap.MonitorAttributeQuery(l, "cn=monitor", "currentconnections") - }, common.CurrentConnectionsGauge, ""}, - - {"total_connections", func(l *ext_ldap.Conn, suffix string) (float64, error) { - return ldap.MonitorAttributeQuery(l, "cn=monitor", "totalconnections") - }, common.TotalConnectionsGauge, ""}, - - {"entries", func(l *ext_ldap.Conn, suffix string) (float64, error) { - return ldap.MonitorAttributeQuery(l, "cn=monitor", "entries") - }, common.EntriesGauge, ""}, - - {"ops_initiated", func(l *ext_ldap.Conn, suffix string) (float64, error) { - return ldap.MonitorAttributeQuery(l, "cn=monitor", "opsinitiated") - }, common.OperationsInitiatedGauge, ""}, - - {"ops_completed", func(l *ext_ldap.Conn, suffix string) (float64, error) { - return ldap.MonitorAttributeQuery(l, "cn=monitor", "opscompleted") - }, common.OperationsCompletedGauge, ""}, - - {"threads", func(l *ext_ldap.Conn, suffix string) (float64, error) { - return ldap.MonitorAttributeQuery(l, "cn=monitor", "threads") - }, common.ThreadsGauge, ""}, - - {"version", func(l *ext_ldap.Conn, suffix string) (float64, error) { - return ldap.MonitorAttributeQuery(l, "cn=monitor", "version") - }, common.VersionGauge, ""}, - - {"bytessent", func(l *ext_ldap.Conn, suffix string) (float64, error) { - return ldap.MonitorAttributeQuery(l, "cn=monitor", "bytessent") - }, common.BytesSentGauge, ""}, -} - -/** - * ScrapeMetrics scrapes all metrics from the LDAP server. - */ -func ScrapeMetrics(ldapAddr, ldapUser, ldapPass, ipaDomain string) { - start := time.Now() - if err := scrapeAllMetrics(ldapAddr, ldapUser, ldapPass, ipaDomain); err != nil { - common.ScrapeCounter.WithLabelValues("fail").Inc() - log.Error("scrape failed:", err) - } else { - common.ScrapeCounter.WithLabelValues("ok").Inc() - } - elapsed := time.Since(start).Seconds() - common.ScrapeDurationGauge.WithLabelValues().Set(float64(elapsed)) - log.Infof("scrape completed in %f seconds", elapsed) -} - -/** - * Scrape all metrics from the LDAP server. - */ -func scrapeAllMetrics(ldapAddr, ldapUser, ldapPass, ipaDomain string) error { - dnSuffix := common.IpaDomainToBaseDN(ipaDomain) - userWithDn := common.UserWithBaseDN(ldapUser, dnSuffix) - - l, err := ext_ldap.Dial("tcp", ldapAddr) - if err != nil { - return err - } - defer l.Close() - - err = l.Bind(userWithDn, ldapPass) - if err != nil { - return err - } - - var errs error - for _, metric := range metrics { - log.Debugf("getting %s", metric.Name) - num, err := metric.QueryFunc(l, dnSuffix) - if err != nil { - errs = multierror.Append(errs, err) - } - metric.Gauge.WithLabelValues(metric.LabelValue).Set(num) - } - - log.Debug("getting replication agreements") - err = ldap.ReplicationQuery(l, dnSuffix) - if err != nil { - errs = multierror.Append(errs, err) - } - - return errs -} diff --git a/internal/pkg/structs/config.go b/internal/pkg/structs/config.go deleted file mode 100644 index 615e416..0000000 --- a/internal/pkg/structs/config.go +++ /dev/null @@ -1,88 +0,0 @@ -package structs - -type Config struct { - LDAP struct { - Address string `json:"ldap.address"` - User string `json:"user"` - Password string `json:"password"` - } `json:"ldap"` - IPA struct { - Domain string `json:"domain"` - } `json:"ipa"` - Scrape struct { - Interval int `json:"interval"` - } `json:"scrape"` - Server struct { - Active bool `json:"active"` - Address string `json:"address"` - Port string `json:"port"` - } `json:"server"` - Log struct { - Level string `json:"level"` - JSON bool `json:"json"` - } `json:"log"` - Export struct { - File string `json:"file"` - Interval int `json:"interval"` - } `json:"export"` -} - -func NewConfig( - ldapAddr string, - ldapUser string, - ldapPass string, - ipaDomain string, - scrapeInterval int, - serverActive bool, - serverAddr string, - serverPort string, - logLevel string, - logJSON bool, - exportFile string, - exportInterval int, -) *Config { - return &Config{ - LDAP: struct { - Address string `json:"ldap.address"` - User string `json:"user"` - Password string `json:"password"` - }{ - Address: ldapAddr, - User: ldapUser, - Password: ldapPass, - }, - IPA: struct { - Domain string `json:"domain"` - }{ - Domain: ipaDomain, - }, - Scrape: struct { - Interval int `json:"interval"` - }{ - Interval: scrapeInterval, - }, - Server: struct { - Active bool `json:"active"` - Address string `json:"address"` - Port string `json:"port"` - }{ - Active: serverActive, - Address: serverAddr, - Port: serverPort, - }, - Log: struct { - Level string `json:"level"` - JSON bool `json:"json"` - }{ - Level: logLevel, - JSON: logJSON, - }, - Export: struct { - File string `json:"file"` - Interval int `json:"interval"` - }{ - File: exportFile, - Interval: exportInterval, - }, - } -} diff --git a/cmd/root.go b/src/cmd/root.go similarity index 94% rename from cmd/root.go rename to src/cmd/root.go index ae32793..fa7964b 100644 --- a/cmd/root.go +++ b/src/cmd/root.go @@ -25,6 +25,7 @@ func Execute() { var configPath string rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Path to the config file. (--config/-c)") + rootCmd.MarkPersistentFlagRequired("config") common.ViperLoadConfig(configPath, &config) common.SetLogLevel(config.Log.Level, config.Log.JSON) diff --git a/go.mod b/src/go.mod similarity index 94% rename from go.mod rename to src/go.mod index bd6bd01..5509917 100644 --- a/go.mod +++ b/src/go.mod @@ -1,9 +1,8 @@ module go-ldap-metrics-exporter -go 1.21.0 +go 1.21.13 require ( - github.com/hashicorp/go-multierror v1.1.1 github.com/prometheus/client_golang v1.20.5 github.com/prometheus/common v0.62.0 github.com/sirupsen/logrus v1.9.3 @@ -35,7 +34,6 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.6.1 // indirect diff --git a/go.sum b/src/go.sum similarity index 95% rename from go.sum rename to src/go.sum index 7f51564..89ff26e 100644 --- a/go.sum +++ b/src/go.sum @@ -13,11 +13,6 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/internal/exporter/commands.go b/src/internal/exporter/commands.go similarity index 67% rename from internal/exporter/commands.go rename to src/internal/exporter/commands.go index 043aa91..fb12612 100644 --- a/internal/exporter/commands.go +++ b/src/internal/exporter/commands.go @@ -1,6 +1,7 @@ package exporter import ( + "fmt" "go-ldap-metrics-exporter/internal/pkg/app" "go-ldap-metrics-exporter/internal/pkg/structs" "time" @@ -12,8 +13,9 @@ func Start(config *structs.Config) { log.Infof("starting go-ldap-metrics-exporter using LDAP address %s", config.LDAP.Address) if config.Server.Active { - log.Infof("starting prometheus HTTP metrics server on %s", config.Server.Port) - metricsServer := app.StartServer(config.Server.Port, "/metrics") + serverAddrFull := fmt.Sprintf("%s:%s", config.Server.Address, config.Server.Port) + log.Infof("starting prometheus HTTP metrics server on %s", serverAddrFull) + metricsServer := app.StartServer(serverAddrFull, "/metrics") defer app.StopServer(metricsServer) } diff --git a/internal/pkg/app/scaper.go b/src/internal/pkg/app/scrape.go similarity index 88% rename from internal/pkg/app/scaper.go rename to src/internal/pkg/app/scrape.go index 3266b82..4908f5d 100644 --- a/internal/pkg/app/scaper.go +++ b/src/internal/pkg/app/scrape.go @@ -17,14 +17,11 @@ func ScrapeMetrics(config *structs.Config) { prometheus.Init() + log.Infof("scraping metrics from LDAP server %s every %d seconds", config.LDAP.Address, config.Scrape.Interval) + for range ticker.C { log.Debug("starting metrics scrape") - prometheus.ScrapeMetrics( - config.LDAP.Address, - config.LDAP.User, - config.LDAP.Password, - config.IPA.Domain, - ) + prometheus.ScrapeMetrics(config) } } diff --git a/internal/pkg/app/server.go b/src/internal/pkg/app/server.go similarity index 100% rename from internal/pkg/app/server.go rename to src/internal/pkg/app/server.go diff --git a/src/internal/pkg/common/gauge.go b/src/internal/pkg/common/gauge.go new file mode 100644 index 0000000..c7fb9bc --- /dev/null +++ b/src/internal/pkg/common/gauge.go @@ -0,0 +1,83 @@ +package common + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +var ( + /* dn: cn=replication,cn=monitor */ + ReplicationConflictsGauge = newGaugeVec("replicationconflicts", "Number of ldap conflicts", []string{}) + ReplicationStatusGauge = newGaugeVec("replicationstatus", "Replication status by server", []string{"server"}) + ScrapeCounter = newGaugeVec("scrapecount", "successful vs unsuccessful ldap scrape attempts", []string{"result"}) + ScrapeDurationGauge = newGaugeVec("scrapedurationseconds", "time taken per scrape", []string{}) + + /* dn: cn=monitor */ + CurrentConnectionsGauge = newGaugeVec("currentconnections", "Current number of connections to the LDAP server", []string{}) + TotalConnectionsGauge = newGaugeVec("totalconnections", "Total number of connections to the LDAP server", []string{}) + CurrentConnectionsAtMaxThreadsGauge = newGaugeVec("currentconnectionsatmaxthreads", "Current number of connections at max threads", []string{}) + MaxThreadsPerConnHitsGauge = newGaugeVec("maxthreadsperconnhits", "Max threads per connection", []string{}) + DTableSizeGauge = newGaugeVec("dtablesize", "Size of the dtable", []string{}) + ReadWaitersGauge = newGaugeVec("readwaiters", "Number of read waiters", []string{}) + OpsInitiatedGauge = newGaugeVec("opsinitiated", "Number of operations initiated", []string{}) + OpsCompletedGauge = newGaugeVec("opscompleted", "Number of operations completed", []string{}) + EntriesSentGauge = newGaugeVec("entriessent", "Number of entries sent", []string{}) + BytesSentGauge = newGaugeVec("bytessent", "Number of bytes sent", []string{}) + CurrentTimeGauge = newGaugeVec("currenttime", "Current time", []string{}) + StartTimeGauge = newGaugeVec("starttime", "Start time", []string{}) + NBackendsGauge = newGaugeVec("nbackends", "Number of backends", []string{}) + + /* dn: cn=snmp,cn=monitor */ + AnonymousBindsGauge = newGaugeVec("anonymousbinds", "Number of anonymous binds", []string{}) + UnauthBindsGauge = newGaugeVec("unauthbinds", "Number of unauthenticated binds", []string{}) + SimpleAuthBindsGauge = newGaugeVec("simpleauthbinds", "Number of simple authenticated binds", []string{}) + StrongAuthBindsGauge = newGaugeVec("strongauthbinds", "Number of strong authenticated binds", []string{}) + BindSecurityErrorsGauge = newGaugeVec("bindsecurityerrors", "Number of bind security errors", []string{}) + InOpsGauge = newGaugeVec("inops", "Number of incoming operations", []string{}) + ListOpsGauge = newGaugeVec("listops", "Number of list operations", []string{}) + ReadOpsGauge = newGaugeVec("readops", "Number of read operations", []string{}) + CompareOpsGauge = newGaugeVec("compareops", "Number of compare operations", []string{}) + AddEntryOpsGauge = newGaugeVec("addentryops", "Number of add entry operations", []string{}) + ModifyEntryOpsGauge = newGaugeVec("modifyentryops", "Number of modify entry operations", []string{}) + RemoveEntryOpsGauge = newGaugeVec("removeentryops", "Number of remove entry operations", []string{}) + ModifyRDNOpsGauge = newGaugeVec("modifyrdnops", "Number of modify rdn operations", []string{}) + SearchOpsGauge = newGaugeVec("searchops", "Number of search operations", []string{}) + OneLevelSearchOpsGauge = newGaugeVec("onelevelsearchops", "Number of one level search operations", []string{}) + WholeSubtreeSearchOpsGauge = newGaugeVec("wholesubtreesearchops", "Number of whole subtree search operations", []string{}) + ReferralsGauge = newGaugeVec("referrals", "Number of referral operations", []string{}) + ChainingsGauge = newGaugeVec("chainings", "Number of chaining operations", []string{}) + SecurityErrorsGauge = newGaugeVec("securityerrors", "Number of security errors", []string{}) + ErrorsGauge = newGaugeVec("errors", "Number of errors", []string{}) + ConnectionsGauge = newGaugeVec("connections", "Number of connections", []string{}) + ConnectionsInMaxThreadsGauge = newGaugeVec("connectionsinmaxthreads", "Number of connections at max threads", []string{}) + ConnectionsMaxThreadsCountGauge = newGaugeVec("connectionsmaxthreadsount", "Max number of connections", []string{}) + ConnectionsEqGauge = newGaugeVec("connectionseq", "Number of connections equal", []string{}) + BytesRecvGauge = newGaugeVec("bytesrecv", "Number of bytes received", []string{}) + EntriesReturnedGauge = newGaugeVec("entriesreturned", "Number of entries returned", []string{}) + ReferralsReturnedGauge = newGaugeVec("referralsreturned", "Number of referrals returned", []string{}) + SupplierEntriesGauge = newGaugeVec("supplierentries", "Number of supplier entries", []string{}) + CopyEntriesGauge = newGaugeVec("copyentries", "Number of copy entries", []string{}) + CacheEntriesGauge = newGaugeVec("cacheentries", "Number of cache entries", []string{}) + CacheHitsGauge = newGaugeVec("cachehits", "Number of cache hits", []string{}) + ConsumerHitsGauge = newGaugeVec("consumerhits", "Number of consumer hits", []string{}) + + /* dn: cn=disk space,cn=monitor */ + DsDiskGauge = newGaugeVec("dsdisk", "Disk space used", []string{"partition", "metric_type"}) +) + +const ( + subsystem = "ldap_389ds" +) + +/** + * Create a new gauge metric with labels. + */ +func newGaugeVec(name, help string, labels []string) *prometheus.GaugeVec { + return prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Subsystem: subsystem, + Name: name, + Help: help, + }, + labels, + ) +} diff --git a/internal/pkg/common/util.go b/src/internal/pkg/common/util.go similarity index 100% rename from internal/pkg/common/util.go rename to src/internal/pkg/common/util.go diff --git a/src/internal/pkg/ldap/query.go b/src/internal/pkg/ldap/query.go new file mode 100644 index 0000000..15981ed --- /dev/null +++ b/src/internal/pkg/ldap/query.go @@ -0,0 +1,152 @@ +package ldap + +import ( + "fmt" + "go-ldap-metrics-exporter/internal/pkg/common" + "strconv" + "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" + "gopkg.in/ldap.v2" +) + +/** + * Query the LDAP server for replication status. + */ +func CollectReplicationMetrics(l *ldap.Conn, suffix string, gauge *prometheus.GaugeVec) error { + base_dn := fmt.Sprintf("cn=replica,cn=%s,cn=mapping tree,cn=config", suffix) + + req := ldap.NewSearchRequest( + base_dn, ldap.ScopeSingleLevel, + ldap.NeverDerefAliases, 0, 0, false, + "(objectClass=nsds5replicationagreement)", + []string{"nsDS5ReplicaHost", "nsds5replicaLastUpdateStatus", "nsds5replicaConflictCount"}, + nil, + ) + sr, err := l.Search(req) + if err != nil { + return err + } + + for _, entry := range sr.Entries { + host := entry.GetAttributeValue("nsDS5ReplicaHost") + status := entry.GetAttributeValue("nsds5replicaLastUpdateStatus") + conflict_str := entry.GetAttributeValue("nsds5replicaConflictCount") + + if strings.Contains(status, "Incremental update succeeded") { + gauge.WithLabelValues(host).Set(1) + } else { + log.Warnf("Replication issue detected on host: %s, status: %s", host, status) + gauge.WithLabelValues(host).Set(0) + } + + conflict_count, err := strconv.ParseFloat(conflict_str, 64) + if err != nil { + log.Warnf("Failed to parse replication conflict count for host: %s, error: %v", host, err) + conflict_count = -1 + } + common.ReplicationConflictsGauge.WithLabelValues(host).Set(conflict_count) + } + + return nil +} + +func parseMonitorAttributes(attrValue string) (map[string]string, error) { + result := make(map[string]string) + parts := strings.Fields(attrValue) + + for _, part := range parts { + keyVal := strings.SplitN(part, "=", 2) + if len(keyVal) != 2 { + return nil, fmt.Errorf("invalid attribute format: %s", part) + } + key := keyVal[0] + value := strings.Trim(keyVal[1], "\"") + result[key] = value + } + return result, nil +} + +func parseLDAPTime(ldapTime string) (float64, error) { + layout := "20060102150405Z" + t, err := time.Parse(layout, ldapTime) + if err != nil { + return 0, fmt.Errorf("failed to parse LDAP time: %s", ldapTime) + } + return float64(t.Unix()), nil +} + +/** + * Query the LDAP server for a specific monitor attribute. + */ +func CollectMonitorMetrics(l *ldap.Conn, base_dn string, attribute string, gauge *prometheus.GaugeVec) { + req := ldap.NewSearchRequest( + base_dn, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, 0, 0, false, + "(objectClass=*)", + []string{attribute}, + nil, + ) + sr, err := l.Search(req) + if err != nil { + log.Errorf("Failed to query monitor attribute %s: %v", attribute, err) + return + } + + if len(sr.Entries) == 0 { + log.Warnf("No entries found for monitor attribute %s", attribute) + return + } + + val := sr.Entries[0].GetAttributeValue(attribute) + if val == "" { + log.Warnf("No value found for monitor attribute %s", attribute) + return + } + + // special cases for attributes that need custom parsing + switch attribute { + case "dsdisk": + parsedAttributes, err := parseMonitorAttributes(val) + if err != nil { + log.Errorf("Failed to parse monitor attribute %s: %v", attribute, err) + return + } + + partition := parsedAttributes["partition"] + size, _ := strconv.ParseFloat(parsedAttributes["size"], 64) + used, _ := strconv.ParseFloat(parsedAttributes["used"], 64) + available, _ := strconv.ParseFloat(parsedAttributes["available"], 64) + usePercent, _ := strconv.ParseFloat(parsedAttributes["use%"], 64) + + gauge.WithLabelValues(partition, "size").Set(size) + gauge.WithLabelValues(partition, "used").Set(used) + gauge.WithLabelValues(partition, "available").Set(available) + gauge.WithLabelValues(partition, "use_percent").Set(usePercent) + log.Debugf("Collected disk metrics for partition %s: size=%f, used=%f, available=%f, use%%=%f", partition, size, used, available, usePercent) + return + + case "starttime", "currenttime": + timestamp, err := parseLDAPTime(val) + if err != nil { + log.Errorf("Failed to parse monitor attribute %s: %v", attribute, err) + return + } + gauge.WithLabelValues().Set(timestamp) + log.Debugf("Collected timestamp metric %s: %f", attribute, timestamp) + return + } + + // directly parse as float + num, err := strconv.ParseFloat(val, 64) + if err != nil { + log.Errorf("Failed to parse monitor attribute %s: %v", attribute, err) + return + } + + gauge.WithLabelValues().Set(num) + log.Debugf("Collected metric %s: %f", attribute, num) +} diff --git a/src/internal/pkg/prometheus/init.go b/src/internal/pkg/prometheus/init.go new file mode 100644 index 0000000..4bf2195 --- /dev/null +++ b/src/internal/pkg/prometheus/init.go @@ -0,0 +1,71 @@ +package prometheus + +import ( + "go-ldap-metrics-exporter/internal/pkg/common" + + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" +) + +func Init() { + log.Info("initializing Prometheus metrics") + registerMetrics() +} + +/** + * Register all desired metrics with Prometheus. + */ +func registerMetrics() { + prometheus.MustRegister( + common.ReplicationConflictsGauge, + common.ReplicationStatusGauge, + common.ScrapeCounter, + common.ScrapeDurationGauge, + common.CurrentConnectionsGauge, + common.TotalConnectionsGauge, + common.CurrentConnectionsAtMaxThreadsGauge, + common.MaxThreadsPerConnHitsGauge, + common.DTableSizeGauge, + common.ReadWaitersGauge, + common.OpsInitiatedGauge, + common.OpsCompletedGauge, + common.EntriesSentGauge, + common.BytesSentGauge, + common.CurrentTimeGauge, + common.StartTimeGauge, + common.NBackendsGauge, + common.AnonymousBindsGauge, + common.UnauthBindsGauge, + common.SimpleAuthBindsGauge, + common.StrongAuthBindsGauge, + common.BindSecurityErrorsGauge, + common.InOpsGauge, + common.ListOpsGauge, + common.ReadOpsGauge, + common.CompareOpsGauge, + common.AddEntryOpsGauge, + common.ModifyEntryOpsGauge, + common.RemoveEntryOpsGauge, + common.ModifyRDNOpsGauge, + common.SearchOpsGauge, + common.OneLevelSearchOpsGauge, + common.WholeSubtreeSearchOpsGauge, + common.ReferralsGauge, + common.ChainingsGauge, + common.SecurityErrorsGauge, + common.ErrorsGauge, + common.ConnectionsGauge, + common.ConnectionsInMaxThreadsGauge, + common.ConnectionsMaxThreadsCountGauge, + common.ConnectionsEqGauge, + common.BytesRecvGauge, + common.EntriesReturnedGauge, + common.ReferralsReturnedGauge, + common.SupplierEntriesGauge, + common.CopyEntriesGauge, + common.CacheEntriesGauge, + common.CacheHitsGauge, + common.ConsumerHitsGauge, + common.DsDiskGauge, + ) +} diff --git a/src/internal/pkg/prometheus/metrics.go b/src/internal/pkg/prometheus/metrics.go new file mode 100644 index 0000000..b6a059a --- /dev/null +++ b/src/internal/pkg/prometheus/metrics.go @@ -0,0 +1,81 @@ +package prometheus + +import ( + "go-ldap-metrics-exporter/internal/pkg/common" + "go-ldap-metrics-exporter/internal/pkg/ldap" + + "github.com/prometheus/client_golang/prometheus" + ext_ldap "gopkg.in/ldap.v2" +) + +type Metric struct { + Name string + Gauge *prometheus.GaugeVec + Suffix string +} + +func collectMetric(l *ext_ldap.Conn, metric Metric) { + ldap.CollectMonitorMetrics(l, metric.Suffix, metric.Name, metric.Gauge) +} + +var replication_metrics = []Metric{ + + /* cn=replication,cn=monitor */ + {"replicationconflicts", common.ReplicationConflictsGauge, "cn=replication,cn=monitor"}, + {"replicationstatus", common.ReplicationStatusGauge, "cn=replication,cn=monitor"}, +} + +var monitor_metrics = []Metric{ + + /* cn=monitor */ + {"currentconnections", common.CurrentConnectionsGauge, "cn=monitor"}, + {"totalconnections", common.TotalConnectionsGauge, "cn=monitor"}, + {"currentconnectionsatmaxthreads", common.CurrentConnectionsAtMaxThreadsGauge, "cn=monitor"}, + {"maxthreadsperconnhits", common.MaxThreadsPerConnHitsGauge, "cn=monitor"}, + {"dtablesize", common.DTableSizeGauge, "cn=monitor"}, + {"readwaiters", common.ReadWaitersGauge, "cn=monitor"}, + {"opsinitiated", common.OpsInitiatedGauge, "cn=monitor"}, + {"opscompleted", common.OpsCompletedGauge, "cn=monitor"}, + {"entriessent", common.EntriesSentGauge, "cn=monitor"}, + {"bytessent", common.BytesSentGauge, "cn=monitor"}, + {"currenttime", common.CurrentTimeGauge, "cn=monitor"}, + {"starttime", common.StartTimeGauge, "cn=monitor"}, + {"nbackends", common.NBackendsGauge, "cn=monitor"}, + + /* cn=snmp,cn=monitor */ + {"anonymousbinds", common.AnonymousBindsGauge, "cn=snmp,cn=monitor"}, + {"unauthbinds", common.UnauthBindsGauge, "cn=snmp,cn=monitor"}, + {"simpleauthbinds", common.SimpleAuthBindsGauge, "cn=snmp,cn=monitor"}, + {"strongauthbinds", common.StrongAuthBindsGauge, "cn=snmp,cn=monitor"}, + {"bindsecurityerrors", common.BindSecurityErrorsGauge, "cn=snmp,cn=monitor"}, + {"inops", common.InOpsGauge, "cn=snmp,cn=monitor"}, + {"listops", common.ListOpsGauge, "cn=snmp,cn=monitor"}, + {"readops", common.ReadOpsGauge, "cn=snmp,cn=monitor"}, + {"compareops", common.CompareOpsGauge, "cn=snmp,cn=monitor"}, + {"addentryops", common.AddEntryOpsGauge, "cn=snmp,cn=monitor"}, + {"modifyentryops", common.ModifyEntryOpsGauge, "cn=snmp,cn=monitor"}, + {"removeentryops", common.RemoveEntryOpsGauge, "cn=snmp,cn=monitor"}, + {"modifyrdnops", common.ModifyRDNOpsGauge, "cn=snmp,cn=monitor"}, + {"searchops", common.SearchOpsGauge, "cn=snmp,cn=monitor"}, + {"onelevelsearchops", common.OneLevelSearchOpsGauge, "cn=snmp,cn=monitor"}, + {"wholesubtreesearchops", common.WholeSubtreeSearchOpsGauge, "cn=snmp,cn=monitor"}, + {"referrals", common.ReferralsGauge, "cn=snmp,cn=monitor"}, + {"chainings", common.ChainingsGauge, "cn=snmp,cn=monitor"}, + {"securityerrors", common.SecurityErrorsGauge, "cn=snmp,cn=monitor"}, + {"errors", common.ErrorsGauge, "cn=snmp,cn=monitor"}, + {"connections", common.ConnectionsGauge, "cn=snmp,cn=monitor"}, + {"connectionsinmaxthreads", common.ConnectionsInMaxThreadsGauge, "cn=snmp,cn=monitor"}, + {"connectionsmaxthreadscount", common.ConnectionsMaxThreadsCountGauge, "cn=snmp,cn=monitor"}, + {"connectionseq", common.ConnectionsEqGauge, "cn=snmp,cn=monitor"}, + {"bytesrecv", common.BytesRecvGauge, "cn=snmp,cn=monitor"}, + {"entriesreturned", common.EntriesReturnedGauge, "cn=snmp,cn=monitor"}, + {"referralsreturned", common.ReferralsReturnedGauge, "cn=snmp,cn=monitor"}, + {"supplierentries", common.SupplierEntriesGauge, "cn=snmp,cn=monitor"}, + {"copyentries", common.CopyEntriesGauge, "cn=snmp,cn=monitor"}, + {"cacheentries", common.CacheEntriesGauge, "cn=snmp,cn=monitor"}, + {"cachehits", common.CacheHitsGauge, "cn=snmp,cn=monitor"}, + {"consumerhits", common.ConsumerHitsGauge, "cn=snmp,cn=monitor"}, + + /* cn=disk space,cn=monitor */ + {"dsdisk", common.DsDiskGauge, "cn=disk space,cn=monitor"}, +} diff --git a/src/internal/pkg/prometheus/scrape.go b/src/internal/pkg/prometheus/scrape.go new file mode 100644 index 0000000..50539d2 --- /dev/null +++ b/src/internal/pkg/prometheus/scrape.go @@ -0,0 +1,130 @@ +package prometheus + +import ( + "errors" + "fmt" + "go-ldap-metrics-exporter/internal/pkg/common" + "go-ldap-metrics-exporter/internal/pkg/structs" + "strings" + "sync" + "time" + + "net/url" + + log "github.com/sirupsen/logrus" + ext_ldap "gopkg.in/ldap.v2" +) + +func ScrapeMetrics(config *structs.Config) { + start := time.Now() + + if err := scrapeAllMetrics(config); err != nil { + common.ScrapeCounter.WithLabelValues("fail").Inc() + log.Error("scrape failed: ", err) + } else { + common.ScrapeCounter.WithLabelValues("ok").Inc() + } + elapsed := time.Since(start).Seconds() + common.ScrapeDurationGauge.WithLabelValues().Set(float64(elapsed)) + log.Debugf("scrape completed in %f seconds", elapsed) +} + +func sanitizeLDAPAddress(ldapAddr string) (string, error) { + if !strings.Contains(ldapAddr, "://") { + return ldapAddr, nil + } + + parsedURL, err := url.Parse(ldapAddr) + if err != nil { + return "", err + } + + host := parsedURL.Host + if host == "" { + return "", fmt.Errorf("missing host in LDAP address") + } + + if !strings.Contains(host, ":") { + if parsedURL.Scheme == "ldaps" { + host += ":636" + } else { + host += ":389" + } + } + + return host, nil +} + +func scrapeAllMetrics(config *structs.Config) error { + sanitizedLDAPAddress, err := sanitizeLDAPAddress(config.LDAP.Address) + if err != nil { + log.Errorf("Invalid LDAP address: %s", err) + return err + } + + l, err := ext_ldap.Dial("tcp", sanitizedLDAPAddress) + if err != nil { + log.Errorf("Failed to connect to LDAP server at %s: %v", sanitizedLDAPAddress, err) + return err + } + defer l.Close() + + fullDn := fmt.Sprintf("uid=%s,%s", config.LDAP.Username, config.LDAP.UserBaseDN) + log.Debugf("Connecting to %s as %s", config.LDAP.Address, fullDn) + + if err := l.Bind(fullDn, config.LDAP.Password); err != nil { + log.Errorf("LDAP bind failed for user %s: %v", fullDn, err) + return err + } + + var errs []error + errs = append(errs, scrapeMetrics(l, replication_metrics)...) + errs = append(errs, scrapeMetrics(l, monitor_metrics)...) + + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} + +func scrapeMetrics(l *ext_ldap.Conn, metrics []Metric) []error { + var errs []error + var mu sync.Mutex + var wg sync.WaitGroup + + for _, metric := range metrics { + wg.Add(1) + go func(metric Metric) { + defer wg.Done() + defer func() { + if r := recover(); r != nil { + log.Errorf("Panic while processing metric %s: %v", metric.Name, r) + mu.Lock() + errs = append(errs, fmt.Errorf("panic while scraping %s: %v", metric.Name, r)) + mu.Unlock() + } + }() + + log.Debugf("Getting '%s'", metric.Name) + + done := make(chan struct{}) + go func() { + collectMetric(l, metric) + close(done) + }() + + select { + case <-done: + log.Debugf("Successfully scraped %s", metric.Name) + case <-time.After(5 * time.Second): + log.Errorf("Metric %s is taking too long!", metric.Name) + mu.Lock() + errs = append(errs, fmt.Errorf("timeout while scraping %s", metric.Name)) + mu.Unlock() + } + }(metric) + } + + wg.Wait() + return errs +} diff --git a/src/internal/pkg/structs/config.go b/src/internal/pkg/structs/config.go new file mode 100644 index 0000000..182af9f --- /dev/null +++ b/src/internal/pkg/structs/config.go @@ -0,0 +1,27 @@ +package structs + +type Config struct { + LDAP struct { + Address string `json:"address"` + Username string `json:"username"` + Password string `json:"password"` + BaseDN string `json:"baseDn"` + UserBaseDN string `json:"userBaseDn"` + } `json:"ldap"` + Scrape struct { + Interval int `json:"interval"` + } `json:"scrape"` + Server struct { + Active bool `json:"active"` + Address string `json:"address"` + Port string `json:"port"` + } `json:"server"` + Log struct { + Level string `json:"level"` + JSON bool `json:"json"` + } `json:"log"` + Export struct { + File string `json:"file"` + Interval int `json:"interval"` + } `json:"export"` +} diff --git a/main.go b/src/main.go similarity index 100% rename from main.go rename to src/main.go