Skip to content

Commit

Permalink
Add new ldap_response input plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
mkboudreau committed Feb 2, 2018
1 parent 0f55d9e commit b0f1292
Show file tree
Hide file tree
Showing 4 changed files with 435 additions and 0 deletions.
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/kafka_consumer_legacy"
_ "github.com/influxdata/telegraf/plugins/inputs/kapacitor"
_ "github.com/influxdata/telegraf/plugins/inputs/kubernetes"
_ "github.com/influxdata/telegraf/plugins/inputs/ldap_response"
_ "github.com/influxdata/telegraf/plugins/inputs/leofs"
_ "github.com/influxdata/telegraf/plugins/inputs/logparser"
_ "github.com/influxdata/telegraf/plugins/inputs/lustre2"
Expand Down
84 changes: 84 additions & 0 deletions plugins/inputs/ldap_response/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Openldap Input Plugin

This plugin gathers metrics from OpenLDAP's cn=Monitor backend.

### Configuration:

To use this plugin you must enable the [monitoring](https://www.openldap.org/devel/admin/monitoringslapd.html) backend.

```toml
[[inputs.openldap]]
host = "localhost"
port = 389

# ldaps, starttls, or no encryption. default is an empty string, disabling all encryption.
# note that port will likely need to be changed to 636 for ldaps
# valid options: "" | "starttls" | "ldaps"
ssl = ""

# skip peer certificate verification. Default is false.
insecure_skip_verify = false

# Path to PEM-encoded Root certificate to use to verify server certificate
ssl_ca = "/etc/ssl/certs.pem"

# dn/password to bind with. If bind_dn is empty, an anonymous bind is performed.
bind_dn = ""
bind_password = ""
```

### Measurements & Fields:

All **monitorCounter**, **monitorOpInitiated**, and **monitorOpCompleted** attributes are gathered based on this LDAP query:

```(|(objectClass=monitorCounterObject)(objectClass=monitorOperation))```

Metric names are based on their entry DN.

Metrics for the **monitorOp*** attributes have **_initiated** and **_completed** added to the base name.

An OpenLDAP 2.4 server will provide these metrics:

- openldap
- max_file_descriptors_connections
- current_connections
- total_connections
- abandon_operations_completed
- abandon_operations_initiated
- add_operations_completed
- add_operations_initiated
- bind_operations_completed
- bind_operations_initiated
- compare_operations_completed
- compare_operations_initiated
- delete_operations_completed
- delete_operations_initiated
- extended_operations_completed
- extended_operations_initiated
- modify_operations_completed
- modify_operations_initiated
- modrdn_operations_completed
- modrdn_operations_initiated
- search_operations_completed
- search_operations_initiated
- unbind_operations_completed
- unbind_operations_initiated
- bytes_statistics
- entries_statistics
- pdu_statistics
- referrals_statistics
- read_waiters
- write_waiters

### Tags:

- server= # value from config
- port= # value from config

### Example Output:

```
$ telegraf -config telegraf.conf -input-filter openldap -test --debug
* Plugin: inputs.openldap, Collection 1
> openldap,server=localhost,port=389,host=zirzla search_operations_completed=2i,delete_operations_completed=0i,read_waiters=1i,total_connections=1004i,bind_operations_completed=3i,unbind_operations_completed=3i,referrals_statistics=0i,current_connections=1i,bind_operations_initiated=3i,compare_operations_completed=0i,add_operations_completed=2i,delete_operations_initiated=0i,unbind_operations_initiated=3i,search_operations_initiated=3i,add_operations_initiated=2i,max_file_descriptors_connections=4096i,abandon_operations_initiated=0i,write_waiters=0i,modrdn_operations_completed=0i,abandon_operations_completed=0i,pdu_statistics=23i,modify_operations_initiated=0i,bytes_statistics=1660i,entries_statistics=17i,compare_operations_initiated=0i,modrdn_operations_initiated=0i,extended_operations_completed=0i,modify_operations_completed=0i,extended_operations_initiated=0i 1499990455000000000
```
202 changes: 202 additions & 0 deletions plugins/inputs/ldap_response/ldap_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package ldap_response

import (
"fmt"
"strconv"
"strings"
"time"

"gopkg.in/ldap.v2"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
)

type Ldap struct {
Host string
Port int
Ssl string
InsecureSkipVerify bool
SslCa string
BindDn string
BindPassword string
SearchBase string
SearchFilter string
SearchAttributes []string
}

const sampleConfig string = `
host = "localhost"
port = 389
# ldaps, starttls, or no encryption. default is an empty string, disabling all encryption.
# note that port will likely need to be changed to 636 for ldaps
# valid options: "" | "starttls" | "ldaps"
ssl = ""
# skip peer certificate verification. Default is false.
insecure_skip_verify = false
# Path to PEM-encoded Root certificate to use to verify server certificate
ssl_ca = "/etc/ssl/certs.pem"
# dn/password to bind with. If bind_dn is empty, an anonymous bind is performed.
bind_dn = ""
bind_password = ""
# base entry for searches
search_base = ""
# ldap search to perform. If search_filter is empty, the bind_dn is used.
search_filter = ""
# the attributes to return as fields
search_attributes = [
"attribute1",
"attribute2",
]
`

var DefaultSearchFilter = "(objectClass=*)"
var DefaultSearchAttributes = []string{"objectclass"}

func (l *Ldap) SampleConfig() string {
return sampleConfig
}

func (l *Ldap) Description() string {
return "LDAP Response Input Plugin"
}

// return an initialized Ldap
func NewLdap() *Ldap {
return &Ldap{
Host: "localhost",
Port: 389,
}
}

// gather metrics
func (l *Ldap) Gather(acc telegraf.Accumulator) error {
var err error
var server *ldap.Conn
beforeConnect := time.Now()
if l.Ssl != "" {
// build tls config
tlsConfig, err := internal.GetTLSConfig("", "", l.SslCa, l.InsecureSkipVerify)
if err != nil {
acc.AddError(err)
return nil
}
if l.Ssl == "ldaps" {
server, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", l.Host, l.Port), tlsConfig)
if err != nil {
acc.AddError(err)
return nil
}
} else if l.Ssl == "starttls" {
server, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", l.Host, l.Port))
if err != nil {
acc.AddError(err)
return nil
}
err = server.StartTLS(tlsConfig)
} else {
acc.AddError(fmt.Errorf("Invalid setting for ssl: %s", l.Ssl))
return nil
}
} else {
server, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", l.Host, l.Port))
}
afterConnect := time.Now()

if err != nil {
acc.AddError(err)
return nil
}
defer server.Close()

// username/password bind
beforeBind := time.Now()
if l.BindDn != "" && l.BindPassword != "" {
err = server.Bind(l.BindDn, l.BindPassword)
if err != nil {
acc.AddError(err)
return nil
}
}
afterBind := time.Now()

if l.SearchFilter == "" {
l.SearchFilter = DefaultSearchFilter
}
if len(l.SearchAttributes) == 0 {
l.SearchAttributes = DefaultSearchAttributes
}

searchRequest := ldap.NewSearchRequest(
l.SearchBase,
ldap.ScopeSingleLevel,
ldap.NeverDerefAliases,
1000,
60,
false,
l.SearchFilter,
l.SearchAttributes,
nil,
)

beforeSearch := time.Now()
sr, err := server.Search(searchRequest)
afterSearch := time.Now()
if err != nil {
acc.AddError(err)
return nil
}

fields := map[string]interface{}{
"connect_time_ms": float64(afterConnect.Sub(beforeConnect).Nanoseconds()) / 1000 / 1000,
"bind_time_ms": float64(afterBind.Sub(beforeBind).Nanoseconds()) / 1000 / 1000,
"query_time_ms": float64(afterSearch.Sub(beforeSearch).Nanoseconds()) / 1000 / 1000,
"total_time_ms": float64(afterSearch.Sub(beforeConnect).Nanoseconds()) / 1000 / 1000,
}

gatherSearchResult(fields, sr, l, acc)

return nil
}

func gatherSearchResult(fields map[string]interface{}, sr *ldap.SearchResult, l *Ldap, acc telegraf.Accumulator) {
tags := map[string]string{
"server": l.Host,
"port": strconv.Itoa(l.Port),
}
for _, entry := range sr.Entries {
metricName := dnToMetric(entry.DN, l.SearchBase)
for _, attr := range entry.Attributes {
if len(attr.Values[0]) >= 1 {
if v, err := strconv.ParseInt(attr.Values[0], 10, 64); err == nil {
fields[metricName+attr.Name] = v
}
}
}
}
acc.AddFields("ldap_response", fields, tags)
return
}

// Convert a DN to metric name, eg cn=Read,cn=Waiters,cn=Monitor to read_waiters
func dnToMetric(dn, searchBase string) string {
metricName := strings.Trim(dn, " ")
metricName = strings.Replace(metricName, " ", "_", -1)
metricName = strings.ToLower(metricName)
metricName = strings.TrimPrefix(metricName, "cn=")
metricName = strings.Replace(metricName, strings.ToLower(searchBase), "", -1)
metricName = strings.Replace(metricName, "cn=", "_", -1)
return strings.Replace(metricName, ",", "", -1)
}

func init() {
inputs.Add("ldap_response", func() telegraf.Input { return NewLdap() })
}
Loading

0 comments on commit b0f1292

Please sign in to comment.