mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
[winsvc] Add support for Windows Eventlog (#26441)
Defines a `winsvc.Event` type which can be sent using the `winsvc.SendEvent` function. If nomad is running on Windows and can send to the Windows Eventlog the event will be sent. Initial event types are defined for starting, ready, stopped, and log message. The `winsvc.EventLogger` provides an `io.WriteCloser` that can be included in the logger's writers collection. It will extract the log level from log lines and write them appropriately to the eventlog. The eventlog only supports error, warning, and info levels so messages with other levels will be ignored. A new configuration block is included for enabling logging to the eventlog. Logging must be enabled with the `log_level` option and the `eventlog.level` value can then be of the same or higher severity.
This commit is contained in:
@@ -75,6 +75,7 @@ func (c *Command) readConfig() *Config {
|
||||
ACL: &ACLConfig{},
|
||||
Audit: &config.AuditConfig{},
|
||||
Reporting: &config.ReportingConfig{},
|
||||
Eventlog: &Eventlog{},
|
||||
}
|
||||
|
||||
flags := flag.NewFlagSet("agent", flag.ContinueOnError)
|
||||
@@ -132,6 +133,10 @@ func (c *Command) readConfig() *Config {
|
||||
flags.BoolVar(&cmdConfig.LogIncludeLocation, "log-include-location", false, "")
|
||||
flags.StringVar(&cmdConfig.NodeName, "node", "", "")
|
||||
|
||||
// Eventlog options
|
||||
flags.BoolVar(&cmdConfig.Eventlog.Enabled, "eventlog", false, "")
|
||||
flags.StringVar(&cmdConfig.Eventlog.Level, "eventlog-level", "", "")
|
||||
|
||||
// Consul options
|
||||
defaultConsul := cmdConfig.defaultConsul()
|
||||
flags.StringVar(&defaultConsul.Auth, "consul-auth", "", "")
|
||||
@@ -489,7 +494,12 @@ func (c *Command) IsValidConfig(config, cmdConfig *Config) bool {
|
||||
return false
|
||||
}
|
||||
if err := config.RPC.Validate(); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("rpc block invalid: %v)", err))
|
||||
c.Ui.Error(fmt.Sprintf("rpc block invalid: %v", err))
|
||||
return false
|
||||
}
|
||||
|
||||
if err := config.Eventlog.Validate(); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("eventlog block invalid: %v", err))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -587,6 +597,7 @@ func SetupLoggers(ui cli.Ui, config *Config) (*gatedwriter.Writer, io.Writer) {
|
||||
if logLevel == "OFF" {
|
||||
config.EnableSyslog = false
|
||||
}
|
||||
|
||||
// Check if syslog is enabled
|
||||
if config.EnableSyslog {
|
||||
ui.Output(fmt.Sprintf("Config enable_syslog is `true` with log_level=%v", config.LogLevel))
|
||||
@@ -598,6 +609,17 @@ func SetupLoggers(ui cli.Ui, config *Config) (*gatedwriter.Writer, io.Writer) {
|
||||
writers = append(writers, newSyslogWriter(l, config.LogJson))
|
||||
}
|
||||
|
||||
// Check if eventlog is enabled
|
||||
if config.Eventlog != nil && config.Eventlog.Enabled {
|
||||
l, err := winsvc.NewEventLogger(config.Eventlog.Level)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Windows event logger setup failed: %s", err))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
writers = append(writers, l)
|
||||
}
|
||||
|
||||
// Check if file logging is enabled
|
||||
if config.LogFile != "" {
|
||||
dir, fileName := filepath.Split(config.LogFile)
|
||||
@@ -773,6 +795,8 @@ func (c *Command) AutocompleteFlags() complete.Flags {
|
||||
"-vault-tls-server-name": complete.PredictAnything,
|
||||
"-acl-enabled": complete.PredictNothing,
|
||||
"-acl-replication-token": complete.PredictAnything,
|
||||
"-eventlog": complete.PredictNothing,
|
||||
"-eventlog-level": complete.PredictSet("INFO", "WARN", "ERROR"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,6 +944,10 @@ func (c *Command) Run(args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Add events for the eventlog
|
||||
winsvc.SendEvent(winsvc.NewEvent(winsvc.EventServiceReady))
|
||||
defer func() { winsvc.SendEvent(winsvc.NewEvent(winsvc.EventServiceStopped)) }()
|
||||
|
||||
// Wait for exit
|
||||
return c.handleSignals()
|
||||
}
|
||||
@@ -1481,6 +1509,14 @@ General Options (clients and servers):
|
||||
-log-include-location
|
||||
Include file and line information in each log line. The default is false.
|
||||
|
||||
-eventlog
|
||||
Enable sending Nomad agent logs to the Windows Event Log.
|
||||
|
||||
-eventlog-level
|
||||
Specifies the verbosity of logs the Nomad agent outputs. Valid log levels
|
||||
include ERROR, WARN, or INFO in order of verbosity. Level must be
|
||||
of equal or less verbosity as defined for the -log-level parameter.
|
||||
|
||||
-node=<name>
|
||||
The name of the local agent. This name is used to identify the node
|
||||
in the cluster. The name must be unique per region. The default is
|
||||
|
||||
@@ -77,6 +77,10 @@ func TestCommand_Args(t *testing.T) {
|
||||
[]string{"-client", "-node-pool=not@valid"},
|
||||
"Invalid node pool",
|
||||
},
|
||||
{
|
||||
[]string{"-client", "-eventlog-level", "DEBUG"},
|
||||
"eventlog.level must be one of INFO, WARN, or ERROR",
|
||||
},
|
||||
}
|
||||
for _, tc := range tcases {
|
||||
// Make a new command. We preemptively close the shutdownCh
|
||||
@@ -502,6 +506,33 @@ func TestIsValidConfig(t *testing.T) {
|
||||
},
|
||||
err: "unknown keyring provider",
|
||||
},
|
||||
{
|
||||
name: "ValidEventlog",
|
||||
conf: Config{
|
||||
DataDir: "/tmp",
|
||||
Client: &ClientConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
Eventlog: &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "INFO",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "InvalidEventlog",
|
||||
conf: Config{
|
||||
DataDir: "/tmp",
|
||||
Client: &ClientConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
Eventlog: &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "DEBUG",
|
||||
},
|
||||
},
|
||||
err: "eventlog.level must be one of INFO, WARN, or ERROR",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/hashicorp/nomad/helper/ipaddr"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/helper/users"
|
||||
"github.com/hashicorp/nomad/helper/winsvc"
|
||||
"github.com/hashicorp/nomad/nomad"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/nomad/structs/config"
|
||||
@@ -199,6 +200,9 @@ type Config struct {
|
||||
|
||||
// ExtraKeysHCL is used by hcl to surface unexpected keys
|
||||
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
|
||||
|
||||
// Configure logging to Windows eventlog
|
||||
Eventlog *Eventlog `hcl:"eventlog"`
|
||||
}
|
||||
|
||||
func (c *Config) defaultConsul() *config.ConsulConfig {
|
||||
@@ -1399,6 +1403,60 @@ func (t *Telemetry) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Eventlog is the configuration for the Windows Eventlog
|
||||
type Eventlog struct {
|
||||
// Enabled controls if Nomad agent logs are sent to the
|
||||
// Windows eventlog.
|
||||
Enabled bool `hcl:"enabled"`
|
||||
// Level of logs to send to eventlog. May be set to higher
|
||||
// severity than LogLevel but lower level will be ignored.
|
||||
Level string `hcl:"level"`
|
||||
}
|
||||
|
||||
// Copy is used to copy the Eventlog configuration
|
||||
func (e *Eventlog) Copy() *Eventlog {
|
||||
return &Eventlog{
|
||||
Enabled: e.Enabled,
|
||||
Level: e.Level,
|
||||
}
|
||||
}
|
||||
|
||||
// Merge is used to merge Eventlog configurations
|
||||
func (e *Eventlog) Merge(b *Eventlog) *Eventlog {
|
||||
if e == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
result := *e
|
||||
|
||||
if b == nil {
|
||||
return &result
|
||||
}
|
||||
|
||||
if b.Enabled {
|
||||
result.Enabled = b.Enabled
|
||||
}
|
||||
|
||||
if b.Level != "" {
|
||||
result.Level = b.Level
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// Validate validates the eventlog configuration
|
||||
func (e *Eventlog) Validate() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if winsvc.EventlogLevelFromString(e.Level) == winsvc.EVENTLOG_LEVEL_UNKNOWN {
|
||||
return errors.New("eventlog.level must be one of INFO, WARN, or ERROR")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ports encapsulates the various ports we bind to for network services. If any
|
||||
// are not specified then the defaults are used instead.
|
||||
type Ports struct {
|
||||
@@ -1732,6 +1790,10 @@ func DefaultConfig() *Config {
|
||||
collectionInterval: 1 * time.Second,
|
||||
DisableAllocationHookMetrics: pointer.Of(false),
|
||||
},
|
||||
Eventlog: &Eventlog{
|
||||
Enabled: false,
|
||||
Level: "error",
|
||||
},
|
||||
TLSConfig: &config.TLSConfig{},
|
||||
Sentinel: &config.SentinelConfig{},
|
||||
Version: version.GetVersion(),
|
||||
@@ -1844,6 +1906,13 @@ func (c *Config) Merge(b *Config) *Config {
|
||||
result.Telemetry = result.Telemetry.Merge(b.Telemetry)
|
||||
}
|
||||
|
||||
// Apply the eventlog config
|
||||
if result.Eventlog == nil && b.Eventlog != nil {
|
||||
result.Eventlog = b.Eventlog.Copy()
|
||||
} else if b.Eventlog != nil {
|
||||
result.Eventlog = result.Eventlog.Merge(b.Eventlog)
|
||||
}
|
||||
|
||||
// Apply the Reporting Config
|
||||
if result.Reporting == nil && b.Reporting != nil {
|
||||
result.Reporting = b.Reporting.Copy()
|
||||
|
||||
@@ -48,6 +48,7 @@ func TestConfig_Merge(t *testing.T) {
|
||||
AdvertiseAddrs: &AdvertiseAddrs{},
|
||||
Sentinel: &config.SentinelConfig{},
|
||||
Autopilot: &config.AutopilotConfig{},
|
||||
Eventlog: &Eventlog{},
|
||||
}
|
||||
|
||||
c2 := &Config{
|
||||
@@ -236,6 +237,10 @@ func TestConfig_Merge(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
Eventlog: &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "INFO",
|
||||
},
|
||||
}
|
||||
|
||||
c3 := &Config{
|
||||
@@ -489,6 +494,10 @@ func TestConfig_Merge(t *testing.T) {
|
||||
Enabled: pointer.Of(true),
|
||||
},
|
||||
},
|
||||
Eventlog: &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "ERROR",
|
||||
},
|
||||
}
|
||||
|
||||
result := c0.Merge(c1)
|
||||
@@ -2013,3 +2022,86 @@ func TestConfig_LoadClientNodeMaxAllocs(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEventlog_Merge(t *testing.T) {
|
||||
t.Run("nil rhs merge", func(t *testing.T) {
|
||||
var c1, c2 *Eventlog
|
||||
c1 = &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "info",
|
||||
}
|
||||
result := c1.Merge(c2)
|
||||
must.Eq(t, result, c1)
|
||||
})
|
||||
|
||||
t.Run("nil lhs merge", func(t *testing.T) {
|
||||
var c1, c2 *Eventlog
|
||||
c2 = &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "info",
|
||||
}
|
||||
result := c1.Merge(c2)
|
||||
must.Eq(t, result, c2)
|
||||
})
|
||||
|
||||
t.Run("full merge", func(t *testing.T) {
|
||||
c1 := &Eventlog{
|
||||
Enabled: false,
|
||||
Level: "info",
|
||||
}
|
||||
c2 := &Eventlog{
|
||||
Enabled: true,
|
||||
Level: "error",
|
||||
}
|
||||
result := c1.Merge(c2)
|
||||
must.True(t, result.Enabled)
|
||||
must.Eq(t, result.Level, "error")
|
||||
})
|
||||
|
||||
t.Run("enabled merge", func(t *testing.T) {
|
||||
// NOTE: Can only be enabled, not disabled
|
||||
c1 := &Eventlog{
|
||||
Enabled: true,
|
||||
}
|
||||
c2 := &Eventlog{
|
||||
Enabled: false,
|
||||
}
|
||||
result := c1.Merge(c2)
|
||||
must.True(t, result.Enabled)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestEventlog_Validate(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testCases := []struct {
|
||||
desc string
|
||||
eventlog *Eventlog
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
desc: "valid level",
|
||||
eventlog: &Eventlog{Level: "info"},
|
||||
},
|
||||
{
|
||||
desc: "invalid level",
|
||||
eventlog: &Eventlog{Level: "debug"},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
desc: "nil eventlog",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
if tc.shouldErr {
|
||||
must.Error(t, tc.eventlog.Validate())
|
||||
} else {
|
||||
must.NoError(t, tc.eventlog.Validate())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
73
helper/winsvc/event.go
Normal file
73
helper/winsvc/event.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
type WindowsEventId uint32
|
||||
|
||||
//go:generate stringer -trimprefix=Event -output strings_eventid.go -linecomment -type=WindowsEventId
|
||||
const (
|
||||
EventUnknown WindowsEventId = iota // unknown event
|
||||
EventServiceStarting // service starting
|
||||
EventServiceReady // service ready
|
||||
EventServiceStopped // service stopped
|
||||
EventLogMessage // log message
|
||||
)
|
||||
|
||||
// NewEvent creates a new Event for the Windows Eventlog
|
||||
func NewEvent(kind WindowsEventId, opts ...EventOption) Event {
|
||||
evt := &event{
|
||||
kind: kind,
|
||||
level: EVENTLOG_LEVEL_INFO,
|
||||
}
|
||||
|
||||
for _, fn := range opts {
|
||||
fn(evt)
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
type Event interface {
|
||||
Kind() WindowsEventId
|
||||
Message() string
|
||||
Level() EventlogLevel
|
||||
}
|
||||
|
||||
type EventOption func(*event)
|
||||
|
||||
// WithEventMessage sets a custom message for the event
|
||||
func WithEventMessage(msg string) EventOption {
|
||||
return func(e *event) {
|
||||
e.message = msg
|
||||
}
|
||||
}
|
||||
|
||||
// WithEventLevel specifies the level used for the event
|
||||
func WithEventLevel(level EventlogLevel) EventOption {
|
||||
return func(e *event) {
|
||||
e.level = level
|
||||
}
|
||||
}
|
||||
|
||||
type event struct {
|
||||
kind WindowsEventId
|
||||
message string
|
||||
level EventlogLevel
|
||||
}
|
||||
|
||||
func (e *event) Kind() WindowsEventId {
|
||||
return e.kind
|
||||
}
|
||||
|
||||
func (e *event) Message() string {
|
||||
if e.message != "" {
|
||||
return e.message
|
||||
}
|
||||
|
||||
return e.kind.String()
|
||||
}
|
||||
|
||||
func (e *event) Level() EventlogLevel {
|
||||
return e.level
|
||||
}
|
||||
106
helper/winsvc/event_logger.go
Normal file
106
helper/winsvc/event_logger.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EventlogLevel uint8
|
||||
|
||||
//go:generate stringer -trimprefix=EVENTLOG_LEVEL_ -output strings_event_logger.go -linecomment -type=EventlogLevel
|
||||
const (
|
||||
EVENTLOG_LEVEL_UNKNOWN EventlogLevel = iota
|
||||
EVENTLOG_LEVEL_INFO
|
||||
EVENTLOG_LEVEL_WARN
|
||||
EVENTLOG_LEVEL_ERROR
|
||||
)
|
||||
|
||||
// EventlogLevelFromString converts a log level string to the correct constant
|
||||
func EventlogLevelFromString(level string) EventlogLevel {
|
||||
switch strings.ToUpper(level) {
|
||||
case EVENTLOG_LEVEL_INFO.String():
|
||||
return EVENTLOG_LEVEL_INFO
|
||||
case EVENTLOG_LEVEL_WARN.String():
|
||||
return EVENTLOG_LEVEL_WARN
|
||||
case EVENTLOG_LEVEL_ERROR.String():
|
||||
return EVENTLOG_LEVEL_ERROR
|
||||
}
|
||||
|
||||
return EVENTLOG_LEVEL_UNKNOWN
|
||||
}
|
||||
|
||||
var logPattern = regexp.MustCompile(`(?s)\[(ERROR|WARN|INFO)\] (.+)`)
|
||||
|
||||
type Eventlog interface {
|
||||
Info(uint32, string) error
|
||||
Warning(uint32, string) error
|
||||
Error(uint32, string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type eventLogger struct {
|
||||
evtLog Eventlog
|
||||
level EventlogLevel
|
||||
}
|
||||
|
||||
// Close closes the eventlog
|
||||
func (e *eventLogger) Close() error {
|
||||
return e.evtLog.Close()
|
||||
}
|
||||
|
||||
// Write writes logging message to the eventlog
|
||||
func (e *eventLogger) Write(p []byte) (int, error) {
|
||||
matches := logPattern.FindStringSubmatch(string(p))
|
||||
|
||||
// If no match was found, or the incorrect number of
|
||||
// elements detected then ignore
|
||||
if matches == nil || len(matches) != 3 {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
level := EventlogLevelFromString(matches[1])
|
||||
|
||||
// If the detected level of the message isn't currently
|
||||
// allowed then ignore
|
||||
if !e.allowed(level) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Still here so send the message to the eventlog
|
||||
switch level {
|
||||
case EVENTLOG_LEVEL_INFO:
|
||||
e.evtLog.Info(uint32(EventLogMessage), matches[2])
|
||||
case EVENTLOG_LEVEL_WARN:
|
||||
e.evtLog.Warning(uint32(EventLogMessage), matches[2])
|
||||
case EVENTLOG_LEVEL_ERROR:
|
||||
e.evtLog.Error(uint32(EventLogMessage), matches[2])
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Check if level is allowed
|
||||
func (e *eventLogger) allowed(level EventlogLevel) bool {
|
||||
return level >= e.level
|
||||
}
|
||||
|
||||
type nullEventlog struct{}
|
||||
|
||||
func (n *nullEventlog) Info(uint32, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nullEventlog) Warning(uint32, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nullEventlog) Error(uint32, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nullEventlog) Close() error {
|
||||
return nil
|
||||
}
|
||||
17
helper/winsvc/event_logger_nonwindows.go
Normal file
17
helper/winsvc/event_logger_nonwindows.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// NewEventLogger is a stub for non-Windows platforms to generate
|
||||
// and error when used.
|
||||
func NewEventLogger(_ string) (io.WriteCloser, error) {
|
||||
return nil, errors.New("EventLogger is not supported on this platform")
|
||||
}
|
||||
114
helper/winsvc/event_logger_test.go
Normal file
114
helper/winsvc/event_logger_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func testEventLogger(e Eventlog, l EventlogLevel) io.WriteCloser {
|
||||
return &eventLogger{
|
||||
level: l,
|
||||
evtLog: e,
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventlogLevelFromString(t *testing.T) {
|
||||
t.Run("INFO", func(t *testing.T) {
|
||||
for _, val := range []string{"INFO", "info"} {
|
||||
l := EventlogLevelFromString(val)
|
||||
must.Eq(t, EVENTLOG_LEVEL_INFO, l)
|
||||
}
|
||||
})
|
||||
t.Run("WARN", func(t *testing.T) {
|
||||
for _, val := range []string{"WARN", "warn"} {
|
||||
l := EventlogLevelFromString(val)
|
||||
must.Eq(t, EVENTLOG_LEVEL_WARN, l)
|
||||
}
|
||||
})
|
||||
t.Run("ERROR", func(t *testing.T) {
|
||||
for _, val := range []string{"ERROR", "error"} {
|
||||
l := EventlogLevelFromString(val)
|
||||
must.Eq(t, EVENTLOG_LEVEL_ERROR, l)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEventLogger(t *testing.T) {
|
||||
defaultmsgs := []string{
|
||||
"1970-01-01T16:27:16.116Z [INFO] Information line",
|
||||
"1970-01-01T16:27:16.116Z [WARN] Warning line",
|
||||
"1970-01-01T16:27:16.116Z [ERROR] Error line",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
msgs []string
|
||||
level EventlogLevel
|
||||
setup func(*MockEventlog)
|
||||
}{
|
||||
{
|
||||
desc: "basic usage",
|
||||
level: EVENTLOG_LEVEL_INFO,
|
||||
setup: func(m *MockEventlog) {
|
||||
m.ExpectInfo(EventLogMessage, "Information line")
|
||||
m.ExpectWarning(EventLogMessage, "Warning line")
|
||||
m.ExpectError(EventLogMessage, "Error line")
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "higher level",
|
||||
level: EVENTLOG_LEVEL_ERROR,
|
||||
setup: func(m *MockEventlog) {
|
||||
m.ExpectError(EventLogMessage, "Error line")
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "debug and trace logs",
|
||||
level: EVENTLOG_LEVEL_INFO,
|
||||
setup: func(m *MockEventlog) {
|
||||
m.ExpectInfo(EventLogMessage, "Information line")
|
||||
m.ExpectWarning(EventLogMessage, "Warning line")
|
||||
m.ExpectError(EventLogMessage, "Error line")
|
||||
},
|
||||
msgs: append(defaultmsgs, []string{
|
||||
"[DEBUG] Debug line",
|
||||
"[TRACE] Trace line",
|
||||
}...),
|
||||
},
|
||||
{
|
||||
desc: "with multi-line logs",
|
||||
level: EVENTLOG_LEVEL_INFO,
|
||||
setup: func(m *MockEventlog) {
|
||||
m.ExpectInfo(EventLogMessage, "Information line")
|
||||
m.ExpectWarning(EventLogMessage, "Warning line")
|
||||
m.ExpectError(EventLogMessage, "Error line")
|
||||
m.ExpectInfo(EventLogMessage, "Information log\nthat includes\nmultiple lines")
|
||||
m.ExpectWarning(EventLogMessage, "Warning log\nthat includes second line")
|
||||
},
|
||||
msgs: append(defaultmsgs, []string{
|
||||
"[INFO] Information log\nthat includes\nmultiple lines",
|
||||
"[WARN] Warning log\nthat includes second line",
|
||||
}...),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if len(tc.msgs) < 1 {
|
||||
tc.msgs = defaultmsgs
|
||||
}
|
||||
|
||||
el := NewMockEventlog(t)
|
||||
tc.setup(el)
|
||||
eventLogger := testEventLogger(el, tc.level)
|
||||
|
||||
for _, msg := range tc.msgs {
|
||||
eventLogger.Write([]byte(msg))
|
||||
}
|
||||
el.AssertExpectations()
|
||||
}
|
||||
}
|
||||
24
helper/winsvc/event_logger_windows.go
Normal file
24
helper/winsvc/event_logger_windows.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
)
|
||||
|
||||
// NewEventLogger creates a new event logger instance
|
||||
func NewEventLogger(level string) (io.WriteCloser, error) {
|
||||
evtLog, err := eventlog.Open(WINDOWS_SERVICE_NAME)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to open Windows eventlog: %w", err)
|
||||
}
|
||||
|
||||
return &eventLogger{
|
||||
evtLog: evtLog,
|
||||
level: EventlogLevelFromString(level),
|
||||
}, nil
|
||||
}
|
||||
43
helper/winsvc/event_test.go
Normal file
43
helper/winsvc/event_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestNewEvent(t *testing.T) {
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
event := NewEvent(EventServiceReady)
|
||||
must.Eq(t, EventServiceReady, event.Kind())
|
||||
must.Eq(t, EVENTLOG_LEVEL_INFO, event.Level())
|
||||
must.Eq(t, EventServiceReady.String(), event.Message())
|
||||
})
|
||||
|
||||
t.Run("WithEventMessage", func(t *testing.T) {
|
||||
event := NewEvent(EventServiceReady, WithEventMessage("Custom service ready message"))
|
||||
must.Eq(t, EventServiceReady, event.Kind())
|
||||
must.Eq(t, EVENTLOG_LEVEL_INFO, event.Level())
|
||||
must.Eq(t, "Custom service ready message", event.Message())
|
||||
})
|
||||
|
||||
t.Run("WithEventLevel", func(t *testing.T) {
|
||||
event := NewEvent(EventServiceReady, WithEventLevel(EVENTLOG_LEVEL_ERROR))
|
||||
must.Eq(t, EventServiceReady, event.Kind())
|
||||
must.Eq(t, EVENTLOG_LEVEL_ERROR, event.Level())
|
||||
must.Eq(t, EventServiceReady.String(), event.Message())
|
||||
})
|
||||
|
||||
t.Run("multiple options", func(t *testing.T) {
|
||||
event := NewEvent(EventServiceStopped,
|
||||
WithEventMessage("Custom service stopped message"),
|
||||
WithEventLevel(EVENTLOG_LEVEL_WARN),
|
||||
)
|
||||
must.Eq(t, EventServiceStopped, event.Kind())
|
||||
must.Eq(t, EVENTLOG_LEVEL_WARN, event.Level())
|
||||
must.Eq(t, "Custom service stopped message", event.Message())
|
||||
})
|
||||
}
|
||||
9
helper/winsvc/events_nonwindows.go
Normal file
9
helper/winsvc/events_nonwindows.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package winsvc
|
||||
|
||||
// SendEvent sends an event to the Windows eventlog
|
||||
func SendEvent(e Event) {}
|
||||
26
helper/winsvc/events_windows.go
Normal file
26
helper/winsvc/events_windows.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/nomad/helper"
|
||||
)
|
||||
|
||||
var chanEvents = make(chan Event)
|
||||
|
||||
// SendEvent sends an event to the Windows eventlog
|
||||
func SendEvent(e Event) {
|
||||
timer, stop := helper.NewSafeTimer(100 * time.Millisecond)
|
||||
defer stop()
|
||||
|
||||
select {
|
||||
case chanEvents <- e:
|
||||
case <-timer.C:
|
||||
hclog.L().Error("failed to send event to windows eventlog, timed out",
|
||||
"event", e)
|
||||
}
|
||||
}
|
||||
84
helper/winsvc/mock_eventlog.go
Normal file
84
helper/winsvc/mock_eventlog.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func NewMockEventlog(t *testing.T) *MockEventlog {
|
||||
return &MockEventlog{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
type MockEventlog struct {
|
||||
infos []mockArgs
|
||||
warnings []mockArgs
|
||||
errors []mockArgs
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
type mockArgs struct {
|
||||
winId uint32
|
||||
msg string
|
||||
}
|
||||
|
||||
func (m *MockEventlog) ExpectInfo(v1 WindowsEventId, v2 string) {
|
||||
m.infos = append(m.infos, mockArgs{uint32(v1), v2})
|
||||
}
|
||||
|
||||
func (m *MockEventlog) ExpectWarning(v1 WindowsEventId, v2 string) {
|
||||
m.warnings = append(m.warnings, mockArgs{uint32(v1), v2})
|
||||
}
|
||||
|
||||
func (m *MockEventlog) ExpectError(v1 WindowsEventId, v2 string) {
|
||||
m.errors = append(m.errors, mockArgs{uint32(v1), v2})
|
||||
}
|
||||
|
||||
func (m *MockEventlog) Info(v1 uint32, v2 string) error {
|
||||
m.t.Helper()
|
||||
|
||||
expectedArgs := m.infos[0]
|
||||
m.infos = m.infos[1:]
|
||||
|
||||
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
|
||||
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockEventlog) Warning(v1 uint32, v2 string) error {
|
||||
m.t.Helper()
|
||||
|
||||
expectedArgs := m.warnings[0]
|
||||
m.warnings = m.warnings[1:]
|
||||
|
||||
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
|
||||
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockEventlog) Error(v1 uint32, v2 string) error {
|
||||
m.t.Helper()
|
||||
|
||||
expectedArgs := m.errors[0]
|
||||
m.errors = m.errors[1:]
|
||||
|
||||
must.Eq(m.t, expectedArgs.winId, v1, must.Sprint("Incorrect WindowsEventId value"))
|
||||
must.Eq(m.t, expectedArgs.msg, v2, must.Sprint("Incorrect message value"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockEventlog) Close() error { return nil }
|
||||
|
||||
func (m *MockEventlog) AssertExpectations() {
|
||||
must.SliceEmpty(m.t, m.infos, must.Sprintf("Info expecting %d more invocations", len(m.infos)))
|
||||
must.SliceEmpty(m.t, m.warnings, must.Sprintf("Warning expecting %d more invocations", len(m.warnings)))
|
||||
must.SliceEmpty(m.t, m.errors, must.Sprintf("Error expecting %d more invocations", len(m.errors)))
|
||||
}
|
||||
@@ -15,10 +15,10 @@ const (
|
||||
WINDOWS_SERVICE_STATE_TIMEOUT = "1m"
|
||||
)
|
||||
|
||||
var chanGraceExit = make(chan int)
|
||||
var chanGraceExit = make(chan struct{})
|
||||
|
||||
// ShutdownChannel returns a channel that sends a message that a shutdown
|
||||
// signal has been received for the service.
|
||||
func ShutdownChannel() <-chan int {
|
||||
func ShutdownChannel() <-chan struct{} {
|
||||
return chanGraceExit
|
||||
}
|
||||
|
||||
@@ -1,46 +1,84 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
wsvc "golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
)
|
||||
|
||||
type serviceWindows struct{}
|
||||
// Commands that are currently supported
|
||||
const SERVICE_ACCEPTED_COMMANDS = svc.AcceptStop | svc.AcceptShutdown
|
||||
|
||||
type serviceWindows struct {
|
||||
evtLog Eventlog
|
||||
}
|
||||
|
||||
func init() {
|
||||
interactive, err := wsvc.IsAnInteractiveSession()
|
||||
isSvc, err := svc.IsWindowsService()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Cannot run as a service when running interactively
|
||||
if interactive {
|
||||
// This should only run when running
|
||||
// as a service
|
||||
if !isSvc {
|
||||
return
|
||||
}
|
||||
go wsvc.Run("", serviceWindows{})
|
||||
|
||||
go executeWindowsService()
|
||||
}
|
||||
|
||||
// Execute implements the Windows service Handler type. It will be
|
||||
// called at the start of the service, and the service will exit
|
||||
// once Execute completes.
|
||||
func (serviceWindows) Execute(args []string, r <-chan wsvc.ChangeRequest, s chan<- wsvc.Status) (svcSpecificEC bool, exitCode uint32) {
|
||||
const accCommands = wsvc.AcceptStop | wsvc.AcceptShutdown
|
||||
s <- wsvc.Status{State: wsvc.Running, Accepts: accCommands}
|
||||
func (srv serviceWindows) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) {
|
||||
s <- svc.Status{State: svc.Running, Accepts: SERVICE_ACCEPTED_COMMANDS}
|
||||
srv.evtLog.Info(uint32(EventServiceStarting), "service starting")
|
||||
LOOP:
|
||||
for {
|
||||
c := <-r
|
||||
switch c.Cmd {
|
||||
case wsvc.Interrogate:
|
||||
s <- c.CurrentStatus
|
||||
case wsvc.Stop, wsvc.Shutdown:
|
||||
s <- wsvc.Status{State: wsvc.StopPending}
|
||||
chanGraceExit <- 1
|
||||
return false, 0
|
||||
select {
|
||||
case c := <-r:
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
s <- c.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
srv.evtLog.Info(uint32(EventLogMessage), "service stop requested")
|
||||
s <- svc.Status{State: svc.StopPending}
|
||||
close(chanGraceExit)
|
||||
}
|
||||
case e := <-chanEvents:
|
||||
switch e.Level() {
|
||||
case EVENTLOG_LEVEL_INFO:
|
||||
srv.evtLog.Info(uint32(e.Kind()), e.Message())
|
||||
case EVENTLOG_LEVEL_WARN:
|
||||
srv.evtLog.Warning(uint32(e.Kind()), e.Message())
|
||||
case EVENTLOG_LEVEL_ERROR:
|
||||
srv.evtLog.Error(uint32(e.Kind()), e.Message())
|
||||
}
|
||||
|
||||
if e.Kind() == EventServiceStopped {
|
||||
break LOOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, 0
|
||||
}
|
||||
|
||||
func executeWindowsService() {
|
||||
var evtLog Eventlog
|
||||
evtLog, err := eventlog.Open(WINDOWS_SERVICE_NAME)
|
||||
if err != nil {
|
||||
// Eventlog will only be available if the
|
||||
// service was properly registered. If the
|
||||
// service was manually setup, it will likely
|
||||
// not have been registered with the eventlog
|
||||
// so it will not be available. In that case
|
||||
// just stub out the eventlog.
|
||||
evtLog = &nullEventlog{}
|
||||
}
|
||||
defer evtLog.Close()
|
||||
|
||||
svc.Run(WINDOWS_SERVICE_NAME, serviceWindows{evtLog: evtLog})
|
||||
}
|
||||
|
||||
26
helper/winsvc/strings_event_logger.go
Normal file
26
helper/winsvc/strings_event_logger.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Code generated by "stringer -trimprefix=EVENTLOG_LEVEL_ -output strings_event_logger.go -linecomment -type=EventlogLevel"; DO NOT EDIT.
|
||||
|
||||
package winsvc
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[EVENTLOG_LEVEL_UNKNOWN-0]
|
||||
_ = x[EVENTLOG_LEVEL_INFO-1]
|
||||
_ = x[EVENTLOG_LEVEL_WARN-2]
|
||||
_ = x[EVENTLOG_LEVEL_ERROR-3]
|
||||
}
|
||||
|
||||
const _EventlogLevel_name = "UNKNOWNINFOWARNERROR"
|
||||
|
||||
var _EventlogLevel_index = [...]uint8{0, 7, 11, 15, 20}
|
||||
|
||||
func (i EventlogLevel) String() string {
|
||||
if i >= EventlogLevel(len(_EventlogLevel_index)-1) {
|
||||
return "EventlogLevel(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _EventlogLevel_name[_EventlogLevel_index[i]:_EventlogLevel_index[i+1]]
|
||||
}
|
||||
27
helper/winsvc/strings_eventid.go
Normal file
27
helper/winsvc/strings_eventid.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Code generated by "stringer -trimprefix=Event -output strings_eventid.go -linecomment -type=WindowsEventId"; DO NOT EDIT.
|
||||
|
||||
package winsvc
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[EventUnknown-0]
|
||||
_ = x[EventServiceStarting-1]
|
||||
_ = x[EventServiceReady-2]
|
||||
_ = x[EventServiceStopped-3]
|
||||
_ = x[EventLogMessage-4]
|
||||
}
|
||||
|
||||
const _WindowsEventId_name = "unknown eventservice startingservice readyservice stoppedlog message"
|
||||
|
||||
var _WindowsEventId_index = [...]uint8{0, 13, 29, 42, 57, 68}
|
||||
|
||||
func (i WindowsEventId) String() string {
|
||||
if i >= WindowsEventId(len(_WindowsEventId_index)-1) {
|
||||
return "WindowsEventId(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _WindowsEventId_name[_WindowsEventId_index[i]:_WindowsEventId_index[i+1]]
|
||||
}
|
||||
Reference in New Issue
Block a user