-
Notifications
You must be signed in to change notification settings - Fork 5.6k
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
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 7400e87
Newer golang.org/x/sys for mgr.ListServices
vlastahajek 1d52da8
Completed basic functionality
vlastahajek 3b4df68
Marked with TODO
vlastahajek b67c645
All services log under same measurement
vlastahajek 017c131
Requirement analysis for windows services
vlastahajek 5f93ba1
Fixed typo, better formation to highligh questions
vlastahajek f895ef3
Adjusted based on feedback from Daniel Nelson
vlastahajek 570f613
Adjusted based on feedback from Daniel Nelson
vlastahajek 45fb743
Adjusted based na analysis feedback:
vlastahajek 96af3b7
Fix: Fixed error reporting
vlastahajek 37c2952
- proper readme for win_services plugin
vlastahajek 03447bc
proof reading
karel-rehor b05aa4a
small english correction
vlastahajek 2bd9a78
using state and startup_mode as fields and display_name as a tag
vlastahajek 3d4e3db
Added win_services unit tests
vlastahajek b88e0e5
Added comment explaining condition for running
vlastahajek 3d00f89
Merge branch 'master' into vh-win-services
vlastahajek 1ab4cdc
- Removing prefix 'service_' from state and startup mode enumerations
vlastahajek b5a5a1f
Edited to reflect actual state
vlastahajek 957015c
Removing prefix 'service_' from state and startup mode enumerations
vlastahajek b54b8a0
Revert "Marked with TODO"
vlastahajek 3b3e973
- Better script description
vlastahajek c568b19
proof read new section on TICK Scripts
karel-rehor e0a8773
- Fixed TICK script
vlastahajek d203d20
Delete obsolete file
vlastahajek 4a0d3e8
Added important note
vlastahajek 75407cc
Typo
vlastahajek d570894
Reformatted with go fmt
vlastahajek 3b3106f
Reformated with go fmt
vlastahajek 721264a
- Implemented PR review feedback
vlastahajek e0dbcab
Changed state and startup mode fields from string to int
vlastahajek 86b989a
- Changed state and startup mode fields from string to int
vlastahajek a96932c
Changed state and startup mode fields from string to int
vlastahajek 3568a10
Merge branch 'master' into vh-win-services
vlastahajek 5eda565
Small improvements based on PR discussion:
vlastahajek 1966f42
Changing existing tests, which test real service manager, to integrat…
vlastahajek ddbdcfc
Added interfaces and real impl wrappers to enable unit testing
vlastahajek 8c75562
Added data driven unit tests with mock implementation
vlastahajek 5b71ba8
Fixed getter name
vlastahajek e444c75
Merge branch 'master' into vh-win-services
vlastahajek 9085744
Added documentation to types
vlastahajek 547cb9b
Support types made internal
vlastahajek 9f6ca38
Merge branch 'master' into vh-win-services
vlastahajek 86cef94
Merged changes from vh-win-services branch from Godeps_windows
vlastahajek 215828e
Proper skipping of tests in short mode
vlastahajek d307493
Unit test don't need admin permission
vlastahajek 2b1ee6d
Win_services folder added to windows tests
vlastahajek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.