github.com/mackerelio/mackerel-agent-plugins@v0.89.3/mackerel-plugin-openldap/lib/openldap.go (about)

     1  package mpopenldap
     2  
     3  import (
     4  	"crypto/tls"
     5  	"errors"
     6  	"flag"
     7  	"fmt"
     8  	"net/url"
     9  	"os"
    10  	"regexp"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	ldap "github.com/go-ldap/ldap/v3"
    17  	mp "github.com/mackerelio/go-mackerel-plugin-helper"
    18  	"github.com/mackerelio/golib/logging"
    19  	"golang.org/x/text/cases"
    20  	"golang.org/x/text/language"
    21  )
    22  
    23  var (
    24  	logger   = logging.GetLogger("metrics.plugin.openldap")
    25  	cnRegexp = regexp.MustCompile("^cn=([^,]+),")
    26  )
    27  
    28  // OpenLDAPPlugin plugin for OpenLDAP
    29  type OpenLDAPPlugin struct {
    30  	Prefix             string
    31  	UseTLS             bool
    32  	InsecureSkipVerify bool
    33  	TargetHost         string
    34  	BindDn             string
    35  	ReplBase           string
    36  	ReplMasterHost     string
    37  	ReplMasterUseTLS   bool
    38  	ReplMasterBind     string
    39  	ReplMasterPass     string
    40  	ReplLocalBind      string
    41  	ReplLocalPass      string
    42  	BindPasswd         string
    43  	Tempfile           string
    44  	l                  *ldap.Conn
    45  }
    46  
    47  func transformKeyName(key string) string {
    48  	results := cnRegexp.FindStringSubmatch(key)
    49  	if len(results) < 2 {
    50  		return ""
    51  	}
    52  	return strings.ReplaceAll(strings.ToLower(strings.TrimSpace(results[1])), " ", "_")
    53  }
    54  
    55  func getStats(sr *ldap.SearchResult, prefix string) map[string]float64 {
    56  	stat := make(map[string]float64)
    57  	for _, entry := range sr.Entries {
    58  		for _, attr := range entry.Attributes {
    59  			key := prefix + transformKeyName(entry.DN) + "_" + attr.Name
    60  			value, err := strconv.ParseFloat(entry.GetAttributeValue(attr.Name), 64)
    61  			if err != nil {
    62  				logger.Warningf("Failed to parse value. %s", err)
    63  			}
    64  			stat[key] = value
    65  		}
    66  	}
    67  	return stat
    68  }
    69  
    70  func fetchOpenldapMetrics(l *ldap.Conn, base, prefix string, attrs []string) (map[string]float64, error) {
    71  	searchRequest := ldap.NewSearchRequest(base, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, "(&(objectClass=*))", attrs, nil)
    72  	sr, err := l.Search(searchRequest)
    73  	if err != nil {
    74  		logger.Errorf("Failed to ldap search. %s.", err)
    75  		return nil, err
    76  	}
    77  	stat := getStats(sr, prefix)
    78  	return stat, nil
    79  }
    80  
    81  func mergeStat(dst, src map[string]float64) {
    82  	for k, v := range src {
    83  		dst[k] = v
    84  	}
    85  }
    86  
    87  func latestCSN(sr *ldap.SearchResult) (time.Time, error) {
    88  	var res time.Time
    89  	if len(sr.Entries) == 0 {
    90  		return res, errors.New("not found CSN")
    91  	}
    92  	entry := sr.Entries[0]
    93  	if len(entry.Attributes) == 0 {
    94  		return res, errors.New("not found CSN")
    95  	}
    96  	attr := entry.Attributes[0]
    97  	vs := entry.GetAttributeValues(attr.Name)
    98  	csns := make([]time.Time, len(vs))
    99  	for i, v := range vs {
   100  		t, err := time.Parse("20060102150405.999999Z", v[0:strings.Index(v, "#")])
   101  		if err != nil {
   102  			return res, err
   103  		}
   104  		csns[i] = t
   105  	}
   106  	sort.Slice(csns, func(i, j int) bool {
   107  		return csns[i].After(csns[j])
   108  	})
   109  	res = csns[0]
   110  	return res, nil
   111  }
   112  
   113  func getLatestCSN(host, base, bind, passwd string, useTLS, insecureSkipVerify bool) (time.Time, error) {
   114  	var l *ldap.Conn
   115  	var err error
   116  	var res time.Time
   117  	if useTLS {
   118  		u := url.URL{Scheme: "ldaps", Host: host}
   119  		l, err = ldap.DialURL(u.String(), ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: insecureSkipVerify}))
   120  	} else {
   121  		u := url.URL{Scheme: "ldap", Host: host}
   122  		l, err = ldap.DialURL(u.String())
   123  	}
   124  	if err != nil {
   125  		return res, err
   126  	}
   127  	err = l.Bind(bind, passwd)
   128  	if err != nil {
   129  		logger.Errorf("Failed to Bind %s, err: %s", bind, err)
   130  		return res, err
   131  	}
   132  	searchRequest := ldap.NewSearchRequest(base, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, "(&(objectClass=*))", []string{"ContextCSN"}, nil)
   133  	sr, err := l.Search(searchRequest)
   134  	l.Close()
   135  	if err != nil {
   136  		logger.Errorf("Failed to ldap search. %s.", err)
   137  		return res, err
   138  	}
   139  	return latestCSN(sr)
   140  
   141  }
   142  
   143  // FetchMetrics interface for mackerelplugin
   144  func (m OpenLDAPPlugin) FetchMetrics() (map[string]interface{}, error) {
   145  	stat := make(map[string]float64)
   146  	if m.ReplBase != "" {
   147  		masterTime, err := getLatestCSN(m.ReplMasterHost, m.ReplBase, m.ReplMasterBind, m.ReplMasterPass, m.ReplMasterUseTLS, m.InsecureSkipVerify)
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  		localTime, err := getLatestCSN(m.TargetHost, m.ReplBase, m.ReplLocalBind, m.ReplLocalPass, m.UseTLS, m.InsecureSkipVerify)
   152  		if err != nil {
   153  			return nil, err
   154  		}
   155  		stat["replication_delay"] = masterTime.Sub(localTime).Seconds()
   156  	}
   157  
   158  	ldapOpes, err := fetchOpenldapMetrics(m.l, "cn=Operations,cn=Monitor", "", []string{"monitorOpInitiated", "monitorOpCompleted"})
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  	mergeStat(stat, ldapOpes)
   163  	ldapWaiters, err := fetchOpenldapMetrics(m.l, "cn=Waiters,cn=Monitor", "waiters_", []string{"monitorCounter"})
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	mergeStat(stat, ldapWaiters)
   168  
   169  	ldapMaxThreads, err := fetchOpenldapMetrics(m.l, "cn=Max,cn=Threads,cn=Monitor", "threads_", []string{"monitoredInfo"})
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	mergeStat(stat, ldapMaxThreads)
   174  	ldapOpenThreads, err := fetchOpenldapMetrics(m.l, "cn=Open,cn=Threads,cn=Monitor", "threads_", []string{"monitoredInfo"})
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  	mergeStat(stat, ldapOpenThreads)
   179  	ldapActiveThreads, err := fetchOpenldapMetrics(m.l, "cn=Active,cn=Threads,cn=Monitor", "threads_", []string{"monitoredInfo"})
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	mergeStat(stat, ldapActiveThreads)
   184  
   185  	ldapStatistics, err := fetchOpenldapMetrics(m.l, "cn=Statistics,cn=Monitor", "statistics_", []string{"monitorCounter"})
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	mergeStat(stat, ldapStatistics)
   190  	ldapTotalConns, err := fetchOpenldapMetrics(m.l, "cn=Total,cn=Connections,cn=Monitor", "connections_", []string{"monitorCounter"})
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	mergeStat(stat, ldapTotalConns)
   195  	ldapCurrentConns, err := fetchOpenldapMetrics(m.l, "cn=Current,cn=Connections,cn=Monitor", "connections_", []string{"monitorCounter"})
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  	mergeStat(stat, ldapCurrentConns)
   200  
   201  	result := make(map[string]interface{})
   202  	for k, v := range stat {
   203  		result[k] = v
   204  	}
   205  	return result, nil
   206  }
   207  
   208  // MetricKeyPrefix interface for PluginWithPrefix
   209  func (m OpenLDAPPlugin) MetricKeyPrefix() string {
   210  	if m.Prefix == "" {
   211  		m.Prefix = "openldap"
   212  	}
   213  	return m.Prefix
   214  }
   215  
   216  // GraphDefinition interface for mackerelplugin
   217  func (m OpenLDAPPlugin) GraphDefinition() map[string]mp.Graphs {
   218  	labelPrefix := cases.Title(language.Und, cases.NoLower).String(m.Prefix)
   219  	graphs := map[string]mp.Graphs{
   220  		"operations_initiated": {
   221  			Label: (labelPrefix + " operations Initiated"),
   222  			Unit:  "integer",
   223  			Metrics: []mp.Metrics{
   224  				{Name: "operations_monitorOpInitiated", Label: "All", Diff: true},
   225  				{Name: "bind_monitorOpInitiated", Label: "Bind", Diff: true},
   226  				{Name: "unbind_monitorOpInitiated", Label: "Unbind", Diff: true},
   227  				{Name: "search_monitorOpInitiated", Label: "Search", Diff: true},
   228  				{Name: "compare_monitorOpInitiated", Label: "Compare", Diff: true},
   229  				{Name: "modify_monitorOpInitiated", Label: "Modify", Diff: true},
   230  				{Name: "modrdn_monitorOpInitiated", Label: "Modrdn", Diff: true},
   231  				{Name: "add_monitorOpInitiated", Label: "Add", Diff: true},
   232  				{Name: "delete_monitorOpInitiated", Label: "Delete", Diff: true},
   233  				{Name: "abandon_monitorOpInitiated", Label: "Abandon", Diff: true},
   234  				{Name: "extended_monitorOpInitiated", Label: "Extended", Diff: true},
   235  			},
   236  		},
   237  		"operations_Completed": {
   238  			Label: (labelPrefix + " operations Completed"),
   239  			Unit:  "integer",
   240  			Metrics: []mp.Metrics{
   241  				{Name: "operations_monitorOpCompleted", Label: "All", Diff: true},
   242  				{Name: "bind_monitorOpCompleted", Label: "Bind", Diff: true},
   243  				{Name: "unbind_monitorOpCompleted", Label: "Unbind", Diff: true},
   244  				{Name: "search_monitorOpCompleted", Label: "Search", Diff: true},
   245  				{Name: "compare_monitorOpCompleted", Label: "Compare", Diff: true},
   246  				{Name: "modify_monitorOpCompleted", Label: "Modify", Diff: true},
   247  				{Name: "modrdn_monitorOpCompleted", Label: "Modrdn", Diff: true},
   248  				{Name: "add_monitorOpCompleted", Label: "Add", Diff: true},
   249  				{Name: "delete_monitorOpCompleted", Label: "Delete", Diff: true},
   250  				{Name: "abandon_monitorOpCompleted", Label: "Abandon", Diff: true},
   251  				{Name: "extended_monitorOpCompleted", Label: "Extended", Diff: true},
   252  			},
   253  		},
   254  		"waiters": {
   255  			Label: (labelPrefix + " waiters"),
   256  			Unit:  "integer",
   257  			Metrics: []mp.Metrics{
   258  				{Name: "waiters_read_monitorCounter", Label: "read", Diff: false},
   259  				{Name: "waiters_write_monitorCounter", Label: "write", Diff: false},
   260  			},
   261  		},
   262  		"threads": {
   263  			Label: (labelPrefix + " threads"),
   264  			Unit:  "integer",
   265  			Metrics: []mp.Metrics{
   266  				{Name: "threads_max_monitoredInfo", Label: "max", Diff: false},
   267  				{Name: "threads_open_monitoredInfo", Label: "open", Diff: false},
   268  				{Name: "threads_active_monitoredInfo", Label: "active", Diff: false},
   269  			},
   270  		},
   271  		"statistics_bytes": {
   272  			Label: (labelPrefix + " statistics bytes"),
   273  			Unit:  "integer",
   274  			Metrics: []mp.Metrics{
   275  				{Name: "statistics_bytes_monitorCounter", Label: "bytes", Diff: true},
   276  			},
   277  		},
   278  		"statistics_pdu": {
   279  			Label: (labelPrefix + " statistics pdu"),
   280  			Unit:  "integer",
   281  			Metrics: []mp.Metrics{
   282  				{Name: "statistics_pdu_monitorCounter", Label: "pdu", Diff: true},
   283  			},
   284  		},
   285  		"statistics_entries": {
   286  			Label: (labelPrefix + " statistics entries"),
   287  			Unit:  "integer",
   288  			Metrics: []mp.Metrics{
   289  				{Name: "statistics_entries_monitorCounter", Label: "entries", Diff: true},
   290  			},
   291  		},
   292  		"statistics_referrals": {
   293  			Label: (labelPrefix + " statistics referrals"),
   294  			Unit:  "integer",
   295  			Metrics: []mp.Metrics{
   296  				{Name: "statistics_referrals_monitorCounter", Label: "referrals", Diff: true},
   297  			},
   298  		},
   299  		"connections": {
   300  			Label: (labelPrefix + " connections"),
   301  			Unit:  "integer",
   302  			Metrics: []mp.Metrics{
   303  				{Name: "connections_total_monitorCounter", Label: "total connection", Diff: true},
   304  				{Name: "connections_current_monitorCounter", Label: "current connection", Diff: false},
   305  			},
   306  		},
   307  	}
   308  	if m.ReplBase != "" {
   309  		graphs["replications"] = mp.Graphs{
   310  			Label: (labelPrefix + " replication delay"),
   311  			Unit:  "integer",
   312  			Metrics: []mp.Metrics{
   313  				{Name: "replication_delay", Label: "replication delay sec", Diff: false},
   314  			},
   315  		}
   316  	}
   317  	return graphs
   318  }
   319  
   320  // Do the plugin
   321  func Do() {
   322  	optHost := flag.String("host", "localhost", "Hostname")
   323  	optPort := flag.String("port", "389", "Port")
   324  	optTLS := flag.Bool("tls", false, "TLS(ldaps)")
   325  	optInsecureSkipVerify := flag.Bool("insecureSkipVerify", false, "TLS accepts any certificate.")
   326  	optReplBase := flag.String("replBase", "", "replication base dn")
   327  	optReplMasterBind := flag.String("replMasterBind", "", "replication master bind dn")
   328  	optReplMasterHost := flag.String("replMasterHost", "", "replication master hostname")
   329  	optReplMasterTLS := flag.Bool("replMasterTLS", false, "replication master TLS(ldaps)")
   330  	optReplMasterPort := flag.String("replMasterPort", "389", "replication master port")
   331  	optReplMasterPass := flag.String("replMasterPW", os.Getenv("OPENLDAP_REPL_MASTER_PASSWORD"), "replication master bind password")
   332  	optReplLocalBind := flag.String("replLocalBind", "", "replicationlocalmaster bind dn")
   333  	optReplLocalPass := flag.String("replLocalPW", os.Getenv("OPENLDAP_REPL_LOCAL_PASSWORD"), "replication local bind password")
   334  	optBindDn := flag.String("bind", "", "bind dn")
   335  	optBindPasswd := flag.String("pw", os.Getenv("OPENLDAP_PASSWORD"), "bind password")
   336  	optTempfile := flag.String("tempfile", "", "Temp file name")
   337  	optPrefix := flag.String("metric-key-prefix", "openldap", "Metric key prefix")
   338  	flag.Parse()
   339  
   340  	var m OpenLDAPPlugin
   341  	m.TargetHost = fmt.Sprintf("%s:%s", *optHost, *optPort)
   342  	m.ReplMasterHost = fmt.Sprintf("%s:%s", *optReplMasterHost, *optReplMasterPort)
   343  	m.UseTLS = *optTLS
   344  	m.InsecureSkipVerify = *optInsecureSkipVerify
   345  	m.ReplBase = *optReplBase
   346  	m.ReplMasterUseTLS = *optReplMasterTLS
   347  	m.ReplMasterBind = *optReplMasterBind
   348  	m.ReplMasterPass = *optReplMasterPass
   349  	m.ReplLocalBind = *optReplLocalBind
   350  	m.ReplLocalPass = *optReplLocalPass
   351  	if m.InsecureSkipVerify {
   352  		m.UseTLS = true
   353  	}
   354  	m.BindDn = *optBindDn
   355  	m.BindPasswd = *optBindPasswd
   356  	m.Prefix = *optPrefix
   357  
   358  	if *optBindDn == "" {
   359  		logger.Errorf("bind is required.")
   360  		flag.PrintDefaults()
   361  		os.Exit(1)
   362  	}
   363  	var err error
   364  	if m.UseTLS {
   365  		u := url.URL{Scheme: "ldaps", Host: m.TargetHost}
   366  		m.l, err = ldap.DialURL(u.String(), ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: m.InsecureSkipVerify}))
   367  	} else {
   368  		u := url.URL{Scheme: "ldap", Host: m.TargetHost}
   369  		m.l, err = ldap.DialURL(u.String())
   370  	}
   371  	if err != nil {
   372  		logger.Errorf("Failed to Dial %s, err: %s", m.TargetHost, err)
   373  		os.Exit(1)
   374  	}
   375  	err = m.l.Bind(m.BindDn, m.BindPasswd)
   376  	if err != nil {
   377  		logger.Errorf("Failed to Bind %s, err: %s", m.BindDn, err)
   378  		os.Exit(1)
   379  	}
   380  	helper := mp.NewMackerelPlugin(m)
   381  	if *optTempfile != "" {
   382  		helper.Tempfile = *optTempfile
   383  	} else {
   384  		helper.SetTempfileByBasename(fmt.Sprintf("mackerel-plugin-openldap-%s", *optHost))
   385  	}
   386  
   387  	helper.Run()
   388  }