Skip to content

Commit

Permalink
Add Windows Services input plugin (#3023)
Browse files Browse the repository at this point in the history
  • Loading branch information
vlastahajek authored and danielnelson committed Aug 7, 2017
1 parent 2a106be commit 09b1f7e
Show file tree
Hide file tree
Showing 8 changed files with 551 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Godeps
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ github.com/yuin/gopher-lua 66c871e454fcf10251c61bf8eff02d0978cae75a
github.com/zensqlmonitor/go-mssqldb ffe5510c6fa5e15e6d983210ab501c815b56b363
golang.org/x/crypto dc137beb6cce2043eb6b5f223ab8bf51c32459f4
golang.org/x/net f2499483f923065a842d38eb4c7f1927e6fc6e6d
golang.org/x/sys a646d33e2ee3172a661fc09bca23bb4889a41bc8
golang.org/x/sys 739734461d1c916b6c72a63d7efda2b27edb369f
golang.org/x/text 506f9d5c962f284575e88337e7d9296d27e729d3
gopkg.in/asn1-ber.v1 4e86f4367175e39f69d9358a5f17b4dda270378d
gopkg.in/fatih/pool.v2 6e328e67893eb46323ad06f0e92cb9536babbabc
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ test:
test-windows:
go test ./plugins/inputs/ping/...
go test ./plugins/inputs/win_perf_counters/...
go test ./plugins/inputs/win_services/...

lint:
go vet ./...
Expand Down
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/varnish"
_ "github.com/influxdata/telegraf/plugins/inputs/webhooks"
_ "github.com/influxdata/telegraf/plugins/inputs/win_perf_counters"
_ "github.com/influxdata/telegraf/plugins/inputs/win_services"
_ "github.com/influxdata/telegraf/plugins/inputs/zfs"
_ "github.com/influxdata/telegraf/plugins/inputs/zipkin"
_ "github.com/influxdata/telegraf/plugins/inputs/zookeeper"
Expand Down
68 changes: 68 additions & 0 deletions plugins/inputs/win_services/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Telegraf Plugin: win_services
Input plugin to report Windows services info.

It requires that Telegraf must be running under the administrator privileges.
### Configuration:

```toml
[[inputs.win_services]]
## Names of the services to monitor. Leave empty to monitor all the available services on the host
service_names = [
"LanmanServer",
"TermService",
]
```

### Measurements & Fields:

- win_services
- state : integer
- startup_mode : integer

The `state` field can have the following values:
- 1 - stopped
- 2 - start pending
- 3 - stop pending
- 4 - running
- 5 - continue pending
- 6 - pause pending
- 7 - paused

The `startup_mode` field can have the following values:
- 0 - boot start
- 1 - system start
- 2 - auto start
- 3 - demand start
- 4 - disabled

### Tags:

- All measurements have the following tags:
- service_name
- display_name

### Example Output:
```
* Plugin: inputs.win_services, Collection 1
> win_services,host=WIN2008R2H401,display_name=Server,service_name=LanmanServer state=4i,startup_mode=2i 1500040669000000000
> win_services,display_name=Remote\ Desktop\ Services,service_name=TermService,host=WIN2008R2H401 state=1i,startup_mode=3i 1500040669000000000
```
### TICK Scripts

A sample TICK script for a notification about a not running service.
It sends a notification whenever any service changes its state to be not _running_ and when it changes that state back to _running_.
The notification is sent via an HTTP POST call.

```
stream
|from()
.database('telegraf')
.retentionPolicy('autogen')
.measurement('win_services')
.groupBy('host','service_name')
|alert()
.crit(lambda: "state" != 4)
.stateChangesOnly()
.message('Service {{ index .Tags "service_name" }} on Host {{ index .Tags "host" }} is in state {{ index .Fields "state" }} ')
.post('http://localhost:666/alert/service')
```
183 changes: 183 additions & 0 deletions plugins/inputs/win_services/win_services.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// +build windows

package win_services

import (
"fmt"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
)

//WinService provides interface for svc.Service
type WinService interface {
Close() error
Config() (mgr.Config, error)
Query() (svc.Status, error)
}

//WinServiceManagerProvider sets interface for acquiring manager instance, like mgr.Mgr
type WinServiceManagerProvider interface {
Connect() (WinServiceManager, error)
}

//WinServiceManager provides interface for mgr.Mgr
type WinServiceManager interface {
Disconnect() error
OpenService(name string) (WinService, error)
ListServices() ([]string, error)
}

//WinSvcMgr is wrapper for mgr.Mgr implementing WinServiceManager interface
type WinSvcMgr struct {
realMgr *mgr.Mgr
}

func (m *WinSvcMgr) Disconnect() error {
return m.realMgr.Disconnect()
}

func (m *WinSvcMgr) OpenService(name string) (WinService, error) {
return m.realMgr.OpenService(name)
}
func (m *WinSvcMgr) ListServices() ([]string, error) {
return m.realMgr.ListServices()
}

//MgProvider is an implementation of WinServiceManagerProvider interface returning WinSvcMgr
type MgProvider struct {
}

func (rmr *MgProvider) Connect() (WinServiceManager, error) {
scmgr, err := mgr.Connect()
if err != nil {
return nil, err
} else {
return &WinSvcMgr{scmgr}, nil
}
}

var sampleConfig = `
## Names of the services to monitor. Leave empty to monitor all the available services on the host
service_names = [
"LanmanServer",
"TermService",
]
`

var description = "Input plugin to report Windows services info."

//WinServices is an implementation if telegraf.Input interface, providing info about Windows Services
type WinServices struct {
ServiceNames []string `toml:"service_names"`
mgrProvider WinServiceManagerProvider
}

type ServiceInfo struct {
ServiceName string
DisplayName string
State int
StartUpMode int
Error error
}

func (m *WinServices) Description() string {
return description
}

func (m *WinServices) SampleConfig() string {
return sampleConfig
}

func (m *WinServices) Gather(acc telegraf.Accumulator) error {

serviceInfos, err := listServices(m.mgrProvider, m.ServiceNames)

if err != nil {
return err
}

for _, service := range serviceInfos {
if service.Error == nil {
fields := make(map[string]interface{})
tags := make(map[string]string)

//display name could be empty, but still valid service
if len(service.DisplayName) > 0 {
tags["display_name"] = service.DisplayName
}
tags["service_name"] = service.ServiceName

fields["state"] = service.State
fields["startup_mode"] = service.StartUpMode

acc.AddFields("win_services", fields, tags)
} else {
acc.AddError(service.Error)
}
}

return nil
}

//listServices gathers info about given services. If userServices is empty, it return info about all services on current Windows host. Any a critical error is returned.
func listServices(mgrProv WinServiceManagerProvider, userServices []string) ([]ServiceInfo, error) {
scmgr, err := mgrProv.Connect()
if err != nil {
return nil, fmt.Errorf("Could not open service manager: %s", err)
}
defer scmgr.Disconnect()

var serviceNames []string
if len(userServices) == 0 {
//Listing service names from system
serviceNames, err = scmgr.ListServices()
if err != nil {
return nil, fmt.Errorf("Could not list services: %s", err)
}
} else {
serviceNames = userServices
}
serviceInfos := make([]ServiceInfo, len(serviceNames))

for i, srvName := range serviceNames {
serviceInfos[i] = collectServiceInfo(scmgr, srvName)
}

return serviceInfos, nil
}

//collectServiceInfo gathers info about a service from WindowsAPI
func collectServiceInfo(scmgr WinServiceManager, serviceName string) (serviceInfo ServiceInfo) {

serviceInfo.ServiceName = serviceName
srv, err := scmgr.OpenService(serviceName)
if err != nil {
serviceInfo.Error = fmt.Errorf("Could not open service '%s': %s", serviceName, err)
return
}
defer srv.Close()

srvStatus, err := srv.Query()
if err == nil {
serviceInfo.State = int(srvStatus.State)
} else {
serviceInfo.Error = fmt.Errorf("Could not query service '%s': %s", serviceName, err)
//finish collecting info on first found error
return
}

srvCfg, err := srv.Config()
if err == nil {
serviceInfo.DisplayName = srvCfg.DisplayName
serviceInfo.StartUpMode = int(srvCfg.StartType)
} else {
serviceInfo.Error = fmt.Errorf("Could not get config of service '%s': %s", serviceName, err)
}
return
}

func init() {
inputs.Add("win_services", func() telegraf.Input { return &WinServices{mgrProvider: &MgProvider{}} })
}
115 changes: 115 additions & 0 deletions plugins/inputs/win_services/win_services_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// +build windows

//these tests must be run under administrator account
package win_services

import (
"github.com/influxdata/telegraf/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows/svc/mgr"
"testing"
)

var InvalidServices = []string{"XYZ1@", "ZYZ@", "SDF_@#"}
var KnownServices = []string{"LanmanServer", "TermService"}

func TestList(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
services, err := listServices(&MgProvider{}, KnownServices)
require.NoError(t, err)
assert.Len(t, services, 2, "Different number of services")
assert.Equal(t, services[0].ServiceName, KnownServices[0])
assert.Nil(t, services[0].Error)
assert.Equal(t, services[1].ServiceName, KnownServices[1])
assert.Nil(t, services[1].Error)
}

func TestEmptyList(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
services, err := listServices(&MgProvider{}, []string{})
require.NoError(t, err)
assert.Condition(t, func() bool { return len(services) > 20 }, "Too few service")
}

func TestListEr(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
services, err := listServices(&MgProvider{}, InvalidServices)
require.NoError(t, err)
assert.Len(t, services, 3, "Different number of services")
for i := 0; i < 3; i++ {
assert.Equal(t, services[i].ServiceName, InvalidServices[i])
assert.NotNil(t, services[i].Error)
}
}

func TestGather(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
ws := &WinServices{KnownServices, &MgProvider{}}
assert.Len(t, ws.ServiceNames, 2, "Different number of services")
var acc testutil.Accumulator
require.NoError(t, ws.Gather(&acc))
assert.Len(t, acc.Errors, 0, "There should be no errors after gather")

for i := 0; i < 2; i++ {
fields := make(map[string]interface{})
tags := make(map[string]string)
si := getServiceInfo(KnownServices[i])
fields["state"] = int(si.State)
fields["startup_mode"] = int(si.StartUpMode)
tags["service_name"] = si.ServiceName
tags["display_name"] = si.DisplayName
acc.AssertContainsTaggedFields(t, "win_services", fields, tags)
}
}

func TestGatherErrors(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
ws := &WinServices{InvalidServices, &MgProvider{}}
assert.Len(t, ws.ServiceNames, 3, "Different number of services")
var acc testutil.Accumulator
require.NoError(t, ws.Gather(&acc))
assert.Len(t, acc.Errors, 3, "There should be 3 errors after gather")
}

func getServiceInfo(srvName string) *ServiceInfo {

scmgr, err := mgr.Connect()
if err != nil {
return nil
}
defer scmgr.Disconnect()

srv, err := scmgr.OpenService(srvName)
if err != nil {
return nil
}
var si ServiceInfo
si.ServiceName = srvName
srvStatus, err := srv.Query()
if err == nil {
si.State = int(srvStatus.State)
} else {
si.Error = err
}

srvCfg, err := srv.Config()
if err == nil {
si.DisplayName = srvCfg.DisplayName
si.StartUpMode = int(srvCfg.StartType)
} else {
si.Error = err
}
srv.Close()
return &si
}
3 changes: 3 additions & 0 deletions plugins/inputs/win_services/win_services_notwindows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// +build !windows

package win_services
Loading

0 comments on commit 09b1f7e

Please sign in to comment.