Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an input plugin to monitor basic info of Windows Services #3023

Merged
merged 48 commits into from Aug 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0db2520
Initial win_services plugin code: a hardcoded service reporting rando…
vlastahajek Jul 10, 2017
7400e87
Newer golang.org/x/sys for mgr.ListServices
vlastahajek Jul 11, 2017
1d52da8
Completed basic functionality
vlastahajek Jul 11, 2017
3b4df68
Marked with TODO
vlastahajek Jul 12, 2017
b67c645
All services log under same measurement
vlastahajek Jul 12, 2017
017c131
Requirement analysis for windows services
vlastahajek Jul 12, 2017
5f93ba1
Fixed typo, better formation to highligh questions
vlastahajek Jul 12, 2017
f895ef3
Adjusted based on feedback from Daniel Nelson
vlastahajek Jul 13, 2017
570f613
Adjusted based on feedback from Daniel Nelson
vlastahajek Jul 13, 2017
45fb743
Adjusted based na analysis feedback:
vlastahajek Jul 13, 2017
96af3b7
Fix: Fixed error reporting
vlastahajek Jul 13, 2017
37c2952
- proper readme for win_services plugin
vlastahajek Jul 13, 2017
03447bc
proof reading
karel-rehor Jul 13, 2017
b05aa4a
small english correction
vlastahajek Jul 13, 2017
2bd9a78
using state and startup_mode as fields and display_name as a tag
vlastahajek Jul 14, 2017
3d4e3db
Added win_services unit tests
vlastahajek Jul 15, 2017
b88e0e5
Added comment explaining condition for running
vlastahajek Jul 15, 2017
3d00f89
Merge branch 'master' into vh-win-services
vlastahajek Jul 16, 2017
1ab4cdc
- Removing prefix 'service_' from state and startup mode enumerations
vlastahajek Jul 16, 2017
b5a5a1f
Edited to reflect actual state
vlastahajek Jul 16, 2017
957015c
Removing prefix 'service_' from state and startup mode enumerations
vlastahajek Jul 16, 2017
b54b8a0
Revert "Marked with TODO"
vlastahajek Jul 16, 2017
3b3e973
- Better script description
vlastahajek Jul 17, 2017
c568b19
proof read new section on TICK Scripts
karel-rehor Jul 17, 2017
e0a8773
- Fixed TICK script
vlastahajek Jul 17, 2017
d203d20
Delete obsolete file
vlastahajek Jul 17, 2017
4a0d3e8
Added important note
vlastahajek Jul 17, 2017
75407cc
Typo
vlastahajek Jul 17, 2017
d570894
Reformatted with go fmt
vlastahajek Jul 18, 2017
3b3106f
Reformated with go fmt
vlastahajek Jul 18, 2017
721264a
- Implemented PR review feedback
vlastahajek Jul 18, 2017
e0dbcab
Changed state and startup mode fields from string to int
vlastahajek Jul 19, 2017
86b989a
- Changed state and startup mode fields from string to int
vlastahajek Jul 19, 2017
a96932c
Changed state and startup mode fields from string to int
vlastahajek Jul 19, 2017
3568a10
Merge branch 'master' into vh-win-services
vlastahajek Jul 19, 2017
5eda565
Small improvements based on PR discussion:
vlastahajek Aug 1, 2017
1966f42
Changing existing tests, which test real service manager, to integrat…
vlastahajek Aug 1, 2017
ddbdcfc
Added interfaces and real impl wrappers to enable unit testing
vlastahajek Aug 1, 2017
8c75562
Added data driven unit tests with mock implementation
vlastahajek Aug 1, 2017
5b71ba8
Fixed getter name
vlastahajek Aug 1, 2017
e444c75
Merge branch 'master' into vh-win-services
vlastahajek Aug 1, 2017
9085744
Added documentation to types
vlastahajek Aug 2, 2017
547cb9b
Support types made internal
vlastahajek Aug 2, 2017
9f6ca38
Merge branch 'master' into vh-win-services
vlastahajek Aug 4, 2017
86cef94
Merged changes from vh-win-services branch from Godeps_windows
vlastahajek Aug 4, 2017
215828e
Proper skipping of tests in short mode
vlastahajek Aug 4, 2017
d307493
Unit test don't need admin permission
vlastahajek Aug 7, 2017
2b1ee6d
Win_services folder added to windows tests
vlastahajek Aug 7, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Godeps
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
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
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
@@ -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
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Easiest way to deal with zero value tags/fields IMO is to return an (ServiceInfo, error) and append if err != nil

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would then need to propagate a service error to Gather, or pass Accumulator here.
I want to keep the collecting info separated from plugin API.

The ServiceInfo structure is a domain model of a service, any error occurred during the collecting of service info is a property of such model.

This allows future enhancement in case a user will want to record also service errors into db. Because I still think that if user wants to monitor a particular service and on some hosts it is not possible due to an error, user has to look into the telegraf log instead into the db.

But maybe not, we will see.

}

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
@@ -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
@@ -0,0 +1,3 @@
// +build !windows

package win_services