-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Windows Services input plugin (#3023)
- Loading branch information
1 parent
2a106be
commit 09b1f7e
Showing
8 changed files
with
551 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
115
plugins/inputs/win_services/win_services_integration_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// +build !windows | ||
|
||
package win_services |
Oops, something went wrong.