diff --git a/receiver/windowsservicereceiver/factory.go b/receiver/windowsservicereceiver/factory.go index a5947f5c7697d..5a72f91396363 100644 --- a/receiver/windowsservicereceiver/factory.go +++ b/receiver/windowsservicereceiver/factory.go @@ -1,24 +1,18 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -//revive:disable:unused-parameter - package windowsservicereceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/windowsservicereceiver" import ( - "context" + "time" "go.opentelemetry.io/collector/component" - "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/scraper/scraperhelper" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/windowsservicereceiver/internal/metadata" ) -func createDefaultConfig() component.Config { - return &Config{} -} - func NewFactory() receiver.Factory { return receiver.NewFactory( metadata.Type, @@ -27,27 +21,13 @@ func NewFactory() receiver.Factory { ) } -func createMetricsReceiver( - _ context.Context, - _ receiver.Settings, - rConf component.Config, - consumer consumer.Metrics, -) (receiver.Metrics, error) { - cfg := rConf.(*Config) - rcvr := newMetricsReceiver(cfg, consumer) - return rcvr, nil -} - -func newMetricsReceiver(*Config, consumer.Metrics) *windowsServiceReceiver { - return &windowsServiceReceiver{} -} - -type windowsServiceReceiver struct{} - -func (*windowsServiceReceiver) Start(context.Context, component.Host) error { - return nil -} - -func (*windowsServiceReceiver) Shutdown(context.Context) error { - return nil +func createDefaultConfig() component.Config { + return &Config{ + ControllerConfig: scraperhelper.ControllerConfig{ + CollectionInterval: 1 * time.Minute, + }, + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + IncludeServices: nil, + ExcludeServices: nil, + } } diff --git a/receiver/windowsservicereceiver/factory_others.go b/receiver/windowsservicereceiver/factory_others.go new file mode 100644 index 0000000000000..34f23c0070592 --- /dev/null +++ b/receiver/windowsservicereceiver/factory_others.go @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:build !windows + +package windowsservicereceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/windowsservicereceiver" + +import ( + "context" + "errors" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/receiver" +) + +var errUnsupportedOS = errors.New("windowsservicereceiver: supported only on Windows") + +func createMetricsReceiver( + _ context.Context, + _ receiver.Settings, + _ component.Config, + _ consumer.Metrics, +) (receiver.Metrics, error) { + return nil, errUnsupportedOS +} diff --git a/receiver/windowsservicereceiver/factory_windows.go b/receiver/windowsservicereceiver/factory_windows.go index b32e2a9db9fce..062b5289979c4 100644 --- a/receiver/windowsservicereceiver/factory_windows.go +++ b/receiver/windowsservicereceiver/factory_windows.go @@ -1,5 +1,45 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 + //go:build windows package windowsservicereceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/windowsservicereceiver" + +import ( + "context" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/scraper" + "go.opentelemetry.io/collector/scraper/scraperhelper" + + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/windowsservicereceiver/internal/metadata" +) + +func createMetricsReceiver( + _ context.Context, + settings receiver.Settings, + cfg component.Config, + next consumer.Metrics, +) (receiver.Metrics, error) { + rcfg := cfg.(*Config) + mb := metadata.NewMetricsBuilder(rcfg.MetricsBuilderConfig, settings) + s := newWindowsServiceScraper(settings, rcfg, mb) + + ms, err := scraper.NewMetrics( + s.scrape, + scraper.WithStart(s.start), + scraper.WithShutdown(s.shutdown), + ) + if err != nil { + return nil, err + } + + return scraperhelper.NewMetricsController( + &rcfg.ControllerConfig, + settings, + next, + scraperhelper.AddScraper(metadata.Type, ms), + ) +} diff --git a/receiver/windowsservicereceiver/go.mod b/receiver/windowsservicereceiver/go.mod index d0a3c0068d559..67dde800eac9b 100644 --- a/receiver/windowsservicereceiver/go.mod +++ b/receiver/windowsservicereceiver/go.mod @@ -13,8 +13,10 @@ require ( go.opentelemetry.io/collector/pdata v1.45.0 go.opentelemetry.io/collector/receiver v1.45.0 go.opentelemetry.io/collector/receiver/receivertest v0.139.0 + go.opentelemetry.io/collector/scraper v0.139.0 go.opentelemetry.io/collector/scraper/scraperhelper v0.139.0 go.uber.org/goleak v1.3.0 + go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/sys v0.36.0 ) @@ -44,13 +46,11 @@ require ( go.opentelemetry.io/collector/pipeline v1.45.0 // indirect go.opentelemetry.io/collector/receiver/receiverhelper v0.139.0 // indirect go.opentelemetry.io/collector/receiver/xreceiver v0.139.0 // indirect - go.opentelemetry.io/collector/scraper v0.139.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect google.golang.org/grpc v1.76.0 // indirect diff --git a/receiver/windowsservicereceiver/scraper.go b/receiver/windowsservicereceiver/scraper.go index 7af09aad3e44b..7343cda4fbb66 100644 --- a/receiver/windowsservicereceiver/scraper.go +++ b/receiver/windowsservicereceiver/scraper.go @@ -1,48 +1,152 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -//revive:disable:unused-parameter //go:build windows package windowsservicereceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/windowsservicereceiver" import ( "context" + "errors" + "time" "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/collector/receiver" + "go.uber.org/multierr" + "go.uber.org/zap" + "golang.org/x/sys/windows" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/windowsservicereceiver/internal/metadata" ) -//nolint:unused type windowsServiceScraper struct { - scm serviceManager - settings receiver.Settings - conf *Config - mb *metadata.MetricsBuilder + logger *zap.Logger + cfg *Config + mb *metadata.MetricsBuilder + mgr serviceManager + includeSet map[string]struct{} + excludeSet map[string]struct{} + + disabled bool } -//nolint:unused -func newWindowsServiceScraper(settings receiver.Settings, _ *Config) windowsServiceScraper { - return windowsServiceScraper{ - settings: settings, - mb: metadata.NewMetricsBuilder(metadata.DefaultMetricsBuilderConfig(), settings), +func newWindowsServiceScraper(settings receiver.Settings, cfg *Config, mb *metadata.MetricsBuilder) *windowsServiceScraper { + ws := &windowsServiceScraper{ + logger: settings.Logger, + cfg: cfg, + mb: mb, + mgr: serviceManager{}, + } + if len(cfg.IncludeServices) > 0 { + ws.includeSet = make(map[string]struct{}, len(cfg.IncludeServices)) + for _, n := range cfg.IncludeServices { + ws.includeSet[n] = struct{}{} + } } + if len(cfg.ExcludeServices) > 0 { + ws.excludeSet = make(map[string]struct{}, len(cfg.ExcludeServices)) + for _, n := range cfg.ExcludeServices { + ws.excludeSet[n] = struct{}{} + } + } + return ws } -//nolint:unused -func (*windowsServiceScraper) start(context.Context, component.Host) (err error) { - return nil +func mapStartTypeToAttr(st StartType) metadata.AttributeStartupMode { + switch st { + case StartBoot: + return metadata.AttributeStartupModeBootStart + case StartSystem: + return metadata.AttributeStartupModeSystemStart + case StartAutomatic: + return metadata.AttributeStartupModeAutoStart + case StartManual: + return metadata.AttributeStartupModeDemandStart + case StartDisabled: + return metadata.AttributeStartupModeDisabled + default: + return metadata.AttributeStartupModeDemandStart + } } -//nolint:unused -func (*windowsServiceScraper) shutdown(context.Context) (err error) { +func (ws *windowsServiceScraper) start(_ context.Context, _ component.Host) error { + if err := ws.mgr.connect(); err != nil { + if errors.Is(err, windows.ERROR_ACCESS_DENIED) { + ws.logger.Warn("windowsservicereceiver: access denied to Service Control Manager; metrics will not be collected", zap.Error(err)) + ws.disabled = true + return nil + } + return err + } return nil } -//nolint:unused -func (ws *windowsServiceScraper) scrape(context.Context) (pmetric.Metrics, error) { - return ws.mb.Emit(), nil +func (ws *windowsServiceScraper) shutdown(_ context.Context) error { + if ws.disabled { + return nil + } + return ws.mgr.disconnect() +} + +func (ws *windowsServiceScraper) allowed(name string) bool { + if len(ws.includeSet) > 0 { + if _, ok := ws.includeSet[name]; !ok { + return false + } + } + if _, banned := ws.excludeSet[name]; banned { + return false + } + return true +} + +func (ws *windowsServiceScraper) scrape(_ context.Context) (pmetric.Metrics, error) { + if ws.disabled { + return ws.mb.Emit(), nil + } + + ts := pcommon.NewTimestampFromTime(time.Now()) + + names, err := ws.mgr.listServices() + if err != nil { + return ws.mb.Emit(), err + } + + var scrapeErr error + + for _, name := range names { + if !ws.allowed(name) { + continue + } + + svc, err := updateService(&ws.mgr, name) + if err != nil { + scrapeErr = multierr.Append(scrapeErr, err) + continue + } + + if err := svc.updateStatus(); err != nil { + _ = svc.close() + scrapeErr = multierr.Append(scrapeErr, err) + continue + } + if err := svc.updateConfig(); err != nil { + _ = svc.close() + scrapeErr = multierr.Append(scrapeErr, err) + continue + } + + val := int64(svc.status.State) + startAttr := mapStartTypeToAttr(svc.config.StartType) + + ws.mb.RecordWindowsServiceStatusDataPoint(ts, val, name, startAttr) + + if err := svc.close(); err != nil { + scrapeErr = multierr.Append(scrapeErr, err) + } + } + + return ws.mb.Emit(), scrapeErr } diff --git a/receiver/windowsservicereceiver/winapi.go b/receiver/windowsservicereceiver/winapi.go index dcccb549d19c6..526409db432fa 100644 --- a/receiver/windowsservicereceiver/winapi.go +++ b/receiver/windowsservicereceiver/winapi.go @@ -1,7 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -//revive:disable:unused-parameter //go:build windows package windowsservicereceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/windowsservicereceiver" @@ -11,44 +10,66 @@ import ( "golang.org/x/sys/windows/svc/mgr" ) -/* -* Functions and structs which are used to interact with the Windows service api. -* -* Primary functions are connecting to the Service Control Manager (SCM) to gather service information on scrape. -* -* Docs may be found at https://learn.microsoft.com/en-us/windows/win32/services/services -* and "https://learn.microsoft.com/en-us/windows/win32/api/winsvc/" -**/ - -// service manager "client" -// -//nolint:unused +type State uint32 + +const ( + StateStopped State = 1 + StateStartPending State = 2 + StateStopPending State = 3 + StateRunning State = 4 + StateContinuePending State = 5 + StatePausePending State = 6 + StatePaused State = 7 +) + +type StartType uint32 + +const ( + StartBoot StartType = 0 + StartSystem StartType = 1 + StartAutomatic StartType = 2 + StartManual StartType = 3 + StartDisabled StartType = 4 +) + +type configEx struct { + StartType StartType + DelayedAutoStart bool +} + type serviceManager struct { - handle windows.Handle // handle to SCM database + svcmgr *mgr.Mgr } -// get SCM database handle -// -//nolint:unused,unparam -func scmConnect() (*serviceManager, error) { - var h windows.Handle - return &serviceManager{ - h, - }, nil +func (sm *serviceManager) connect() error { + m, err := mgr.Connect() + if err != nil { + return err + } + sm.svcmgr = m + return nil } -//nolint:unused -func (*serviceManager) disconnect() error { +func (sm *serviceManager) disconnect() error { + if sm.svcmgr != nil { + return sm.svcmgr.Disconnect() + } return nil } -//nolint:unused -func (*serviceManager) listServices() ([]string, error) { - var s []string - return s, nil +func (sm *serviceManager) listServices() ([]string, error) { + if sm.svcmgr == nil { + return []string{}, nil + } + return sm.svcmgr.ListServices() } -//nolint:unused -func (*serviceManager) openService() (*mgr.Service, error) { - return nil, nil +func (sm *serviceManager) openService(name string) (*mgr.Service, error) { + if sm.svcmgr == nil { + return nil, windows.ERROR_INVALID_HANDLE + } + if name == "" { + return nil, windows.ERROR_INVALID_PARAMETER + } + return sm.svcmgr.OpenService(name) } diff --git a/receiver/windowsservicereceiver/winservice.go b/receiver/windowsservicereceiver/winservice.go index d6afb8281ff02..5e8aff485f543 100644 --- a/receiver/windowsservicereceiver/winservice.go +++ b/receiver/windowsservicereceiver/winservice.go @@ -1,39 +1,68 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -//revive:disable:unused-parameter //go:build windows package windowsservicereceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/windowsservicereceiver" -import "golang.org/x/sys/windows/svc/mgr" +import ( + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" +) -/** -* Windows Service representation and associated functions. These handle -* interacting with the SCM and martialing service information returned by the -* windows api calls. -**/ - -// receiver representation of a service -// -//nolint:unused type winService struct { - service *mgr.Service - serviceStatus uint32 - startType uint32 + mgr *serviceManager + name string + handle *mgr.Service + + status svc.Status + config configEx } -//nolint:unused -func getService(mgr *serviceManager, sname string) (*winService, error) { - return &winService{}, nil +func updateService(m *serviceManager, sname string) (*winService, error) { + s, err := m.openService(sname) + if err != nil { + return nil, err + } + return &winService{ + mgr: m, + name: sname, + handle: s, + }, nil } -//nolint:unused -func (*winService) getStatus() error { +func (ws *winService) updateStatus() error { + if ws.handle == nil { + return nil + } + st, err := ws.handle.Query() + if err != nil { + return err + } + ws.status = st return nil } -//nolint:unused -func (*winService) getConfig() error { +func (ws *winService) updateConfig() error { + if ws.handle == nil { + return nil + } + cfg, err := ws.handle.Config() + if err != nil { + return err + } + ws.config = configEx{ + StartType: StartType(cfg.StartType), + DelayedAutoStart: cfg.DelayedAutoStart, + } return nil } + +func (ws *winService) close() error { + if ws.handle == nil { + return nil + } + err := ws.handle.Close() + ws.handle = nil + return err +}