mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 02:02:43 +00:00 
			
		
		
		
	VAULT-8732: Add log-file to Vault Agent (#17841)
				
					
				
			* Started work on adding log-file support to Agent * Allow log file to be picked up and appended * Use NewLogFile everywhere * Tried to pull out the config aggregation from Agent.Run Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com>
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -119,3 +119,4 @@ website/components/node_modules | ||||
|  | ||||
| .buildcache/ | ||||
| .releaser/ | ||||
| *.log | ||||
|   | ||||
							
								
								
									
										3
									
								
								changelog/17841.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/17841.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ```release-note:feature | ||||
| logging: Vault Agent supports logging to a specified file path via environment variable, CLI or config | ||||
| ``` | ||||
							
								
								
									
										207
									
								
								command/agent.go
									
									
									
									
									
								
							
							
						
						
									
										207
									
								
								command/agent.go
									
									
									
									
									
								
							| @@ -41,11 +41,11 @@ import ( | ||||
| 	"github.com/hashicorp/vault/command/agent/sink/inmem" | ||||
| 	"github.com/hashicorp/vault/command/agent/template" | ||||
| 	"github.com/hashicorp/vault/command/agent/winsvc" | ||||
| 	"github.com/hashicorp/vault/helper/logging" | ||||
| 	"github.com/hashicorp/vault/helper/metricsutil" | ||||
| 	"github.com/hashicorp/vault/internalshared/configutil" | ||||
| 	"github.com/hashicorp/vault/internalshared/listenerutil" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/consts" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/logging" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/useragent" | ||||
| 	"github.com/hashicorp/vault/sdk/logical" | ||||
| 	"github.com/hashicorp/vault/sdk/version" | ||||
| @@ -61,6 +61,12 @@ var ( | ||||
| 	_ cli.CommandAutocomplete = (*AgentCommand)(nil) | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// flagNameAgentExitAfterAuth is used as an Agent specific flag to indicate | ||||
| 	// that agent should exit after a single successful auth | ||||
| 	flagNameAgentExitAfterAuth = "exit-after-auth" | ||||
| ) | ||||
|  | ||||
| type AgentCommand struct { | ||||
| 	*BaseCommand | ||||
|  | ||||
| @@ -80,6 +86,7 @@ type AgentCommand struct { | ||||
|  | ||||
| 	flagConfigs       []string | ||||
| 	flagLogLevel      string | ||||
| 	flagLogFile       string | ||||
| 	flagExitAfterAuth bool | ||||
|  | ||||
| 	flagTestVerifyOnly bool | ||||
| @@ -124,17 +131,24 @@ func (c *AgentCommand) Flags() *FlagSets { | ||||
| 	}) | ||||
|  | ||||
| 	f.StringVar(&StringVar{ | ||||
| 		Name:       "log-level", | ||||
| 		Name:       flagNameLogLevel, | ||||
| 		Target:     &c.flagLogLevel, | ||||
| 		Default:    "info", | ||||
| 		EnvVar:     "VAULT_LOG_LEVEL", | ||||
| 		EnvVar:     EnvVaultLogLevel, | ||||
| 		Completion: complete.PredictSet("trace", "debug", "info", "warn", "error"), | ||||
| 		Usage: "Log verbosity level. Supported values (in order of detail) are " + | ||||
| 			"\"trace\", \"debug\", \"info\", \"warn\", and \"error\".", | ||||
| 	}) | ||||
|  | ||||
| 	f.StringVar(&StringVar{ | ||||
| 		Name:   flagNameLogFile, | ||||
| 		Target: &c.flagLogFile, | ||||
| 		EnvVar: EnvVaultLogFile, | ||||
| 		Usage:  "Path to the log file that Vault should use for logging", | ||||
| 	}) | ||||
|  | ||||
| 	f.BoolVar(&BoolVar{ | ||||
| 		Name:    "exit-after-auth", | ||||
| 		Name:    flagNameAgentExitAfterAuth, | ||||
| 		Target:  &c.flagExitAfterAuth, | ||||
| 		Default: false, | ||||
| 		Usage: "If set to true, the agent will exit with code 0 after a single " + | ||||
| @@ -193,27 +207,6 @@ func (c *AgentCommand) Run(args []string) int { | ||||
| 	if c.flagCombineLogs { | ||||
| 		c.logWriter = os.Stdout | ||||
| 	} | ||||
| 	var level log.Level | ||||
| 	c.flagLogLevel = strings.ToLower(strings.TrimSpace(c.flagLogLevel)) | ||||
| 	switch c.flagLogLevel { | ||||
| 	case "trace": | ||||
| 		level = log.Trace | ||||
| 	case "debug": | ||||
| 		level = log.Debug | ||||
| 	case "notice", "info", "": | ||||
| 		level = log.Info | ||||
| 	case "warn", "warning": | ||||
| 		level = log.Warn | ||||
| 	case "err", "error": | ||||
| 		level = log.Error | ||||
| 	default: | ||||
| 		c.UI.Error(fmt.Sprintf("Unknown log level: %s", c.flagLogLevel)) | ||||
| 		return 1 | ||||
| 	} | ||||
|  | ||||
| 	if c.logger == nil { | ||||
| 		c.logger = logging.NewVaultLoggerWithWriter(c.logWriter, level) | ||||
| 	} | ||||
|  | ||||
| 	// Validation | ||||
| 	if len(c.flagConfigs) != 1 { | ||||
| @@ -221,7 +214,7 @@ func (c *AgentCommand) Run(args []string) int { | ||||
| 		return 1 | ||||
| 	} | ||||
|  | ||||
| 	// Load the configuration | ||||
| 	// Load the configuration file | ||||
| 	config, err := agentConfig.LoadConfig(c.flagConfigs[0]) | ||||
| 	if err != nil { | ||||
| 		c.UI.Error(fmt.Sprintf("Error loading configuration from %s: %s", c.flagConfigs[0], err)) | ||||
| @@ -235,6 +228,7 @@ func (c *AgentCommand) Run(args []string) int { | ||||
| 				"-config flag.")) | ||||
| 		return 1 | ||||
| 	} | ||||
|  | ||||
| 	if config.AutoAuth == nil && config.Cache == nil { | ||||
| 		c.UI.Error("No auto_auth or cache block found in config file") | ||||
| 		return 1 | ||||
| @@ -243,62 +237,29 @@ func (c *AgentCommand) Run(args []string) int { | ||||
| 		c.UI.Info("No auto_auth block found in config file, not starting automatic authentication feature") | ||||
| 	} | ||||
|  | ||||
| 	exitAfterAuth := config.ExitAfterAuth | ||||
| 	f.Visit(func(fl *flag.Flag) { | ||||
| 		if fl.Name == "exit-after-auth" { | ||||
| 			exitAfterAuth = c.flagExitAfterAuth | ||||
| 		} | ||||
| 	}) | ||||
| 	config = c.aggregateConfig(f, config) | ||||
|  | ||||
| 	c.setStringFlag(f, config.Vault.Address, &StringVar{ | ||||
| 		Name:    flagNameAddress, | ||||
| 		Target:  &c.flagAddress, | ||||
| 		Default: "https://127.0.0.1:8200", | ||||
| 		EnvVar:  api.EnvVaultAddress, | ||||
| 	}) | ||||
| 	config.Vault.Address = c.flagAddress | ||||
| 	c.setStringFlag(f, config.Vault.CACert, &StringVar{ | ||||
| 		Name:    flagNameCACert, | ||||
| 		Target:  &c.flagCACert, | ||||
| 		Default: "", | ||||
| 		EnvVar:  api.EnvVaultCACert, | ||||
| 	}) | ||||
| 	config.Vault.CACert = c.flagCACert | ||||
| 	c.setStringFlag(f, config.Vault.CAPath, &StringVar{ | ||||
| 		Name:    flagNameCAPath, | ||||
| 		Target:  &c.flagCAPath, | ||||
| 		Default: "", | ||||
| 		EnvVar:  api.EnvVaultCAPath, | ||||
| 	}) | ||||
| 	config.Vault.CAPath = c.flagCAPath | ||||
| 	c.setStringFlag(f, config.Vault.ClientCert, &StringVar{ | ||||
| 		Name:    flagNameClientCert, | ||||
| 		Target:  &c.flagClientCert, | ||||
| 		Default: "", | ||||
| 		EnvVar:  api.EnvVaultClientCert, | ||||
| 	}) | ||||
| 	config.Vault.ClientCert = c.flagClientCert | ||||
| 	c.setStringFlag(f, config.Vault.ClientKey, &StringVar{ | ||||
| 		Name:    flagNameClientKey, | ||||
| 		Target:  &c.flagClientKey, | ||||
| 		Default: "", | ||||
| 		EnvVar:  api.EnvVaultClientKey, | ||||
| 	}) | ||||
| 	config.Vault.ClientKey = c.flagClientKey | ||||
| 	c.setBoolFlag(f, config.Vault.TLSSkipVerify, &BoolVar{ | ||||
| 		Name:    flagNameTLSSkipVerify, | ||||
| 		Target:  &c.flagTLSSkipVerify, | ||||
| 		Default: false, | ||||
| 		EnvVar:  api.EnvVaultSkipVerify, | ||||
| 	}) | ||||
| 	config.Vault.TLSSkipVerify = c.flagTLSSkipVerify | ||||
| 	c.setStringFlag(f, config.Vault.TLSServerName, &StringVar{ | ||||
| 		Name:    flagTLSServerName, | ||||
| 		Target:  &c.flagTLSServerName, | ||||
| 		Default: "", | ||||
| 		EnvVar:  api.EnvVaultTLSServerName, | ||||
| 	}) | ||||
| 	config.Vault.TLSServerName = c.flagTLSServerName | ||||
| 	// Build the logger using level, format and path | ||||
| 	logLevel, err := logging.ParseLogLevel(config.LogLevel) | ||||
| 	if err != nil { | ||||
| 		c.UI.Error(err.Error()) | ||||
| 		return 1 | ||||
| 	} | ||||
|  | ||||
| 	logFormat, err := logging.ParseLogFormat(config.LogFormat) | ||||
| 	if err != nil { | ||||
| 		c.UI.Error(err.Error()) | ||||
| 		return 1 | ||||
| 	} | ||||
|  | ||||
| 	logCfg := logging.NewLogConfig("agent", logLevel, logFormat, config.LogFile) | ||||
| 	l, err := logging.Setup(logCfg, c.logWriter) | ||||
| 	if err != nil { | ||||
| 		c.UI.Error(err.Error()) | ||||
| 		return 1 | ||||
| 	} | ||||
|  | ||||
| 	c.logger = l | ||||
|  | ||||
| 	infoKeys := make([]string, 0, 10) | ||||
| 	info := make(map[string]string) | ||||
| @@ -855,16 +816,16 @@ func (c *AgentCommand) Run(args []string) int { | ||||
| 		ss := sink.NewSinkServer(&sink.SinkServerConfig{ | ||||
| 			Logger:        c.logger.Named("sink.server"), | ||||
| 			Client:        ahClient, | ||||
| 			ExitAfterAuth: exitAfterAuth, | ||||
| 			ExitAfterAuth: config.ExitAfterAuth, | ||||
| 		}) | ||||
|  | ||||
| 		ts := template.NewServer(&template.ServerConfig{ | ||||
| 			Logger:        c.logger.Named("template.server"), | ||||
| 			LogLevel:      level, | ||||
| 			LogLevel:      logLevel, | ||||
| 			LogWriter:     c.logWriter, | ||||
| 			AgentConfig:   config, | ||||
| 			Namespace:     templateNamespace, | ||||
| 			ExitAfterAuth: exitAfterAuth, | ||||
| 			ExitAfterAuth: config.ExitAfterAuth, | ||||
| 		}) | ||||
|  | ||||
| 		g.Add(func() error { | ||||
| @@ -963,6 +924,84 @@ func (c *AgentCommand) Run(args []string) int { | ||||
| 	return 0 | ||||
| } | ||||
|  | ||||
| // aggregateConfig ensures that the config object accurately reflects the desired | ||||
| // settings as configured by the user. It applies the relevant config setting based | ||||
| // on the precedence (env var overrides file config, cli overrides env var). | ||||
| // It mutates the config object supplied and returns the updated object. | ||||
| func (c *AgentCommand) aggregateConfig(f *FlagSets, config *agentConfig.Config) *agentConfig.Config { | ||||
| 	f.Visit(func(fl *flag.Flag) { | ||||
| 		if fl.Name == flagNameAgentExitAfterAuth { | ||||
| 			config.ExitAfterAuth = c.flagExitAfterAuth | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	c.setStringFlag(f, config.LogFile, &StringVar{ | ||||
| 		Name:   flagNameLogFile, | ||||
| 		EnvVar: EnvVaultLogFile, | ||||
| 		Target: &c.flagLogFile, | ||||
| 	}) | ||||
| 	config.LogFile = c.flagLogFile | ||||
|  | ||||
| 	c.setStringFlag(f, config.LogLevel, &StringVar{ | ||||
| 		Name:   flagNameLogLevel, | ||||
| 		EnvVar: EnvVaultLogLevel, | ||||
| 		Target: &c.flagLogLevel, | ||||
| 	}) | ||||
| 	config.LogLevel = c.flagLogLevel | ||||
|  | ||||
| 	c.setStringFlag(f, config.Vault.Address, &StringVar{ | ||||
| 		Name:    flagNameAddress, | ||||
| 		Target:  &c.flagAddress, | ||||
| 		Default: "https://127.0.0.1:8200", | ||||
| 		EnvVar:  api.EnvVaultAddress, | ||||
| 	}) | ||||
| 	config.Vault.Address = c.flagAddress | ||||
| 	c.setStringFlag(f, config.Vault.CACert, &StringVar{ | ||||
| 		Name:    flagNameCACert, | ||||
| 		Target:  &c.flagCACert, | ||||
| 		Default: "", | ||||
| 		EnvVar:  api.EnvVaultCACert, | ||||
| 	}) | ||||
| 	config.Vault.CACert = c.flagCACert | ||||
| 	c.setStringFlag(f, config.Vault.CAPath, &StringVar{ | ||||
| 		Name:    flagNameCAPath, | ||||
| 		Target:  &c.flagCAPath, | ||||
| 		Default: "", | ||||
| 		EnvVar:  api.EnvVaultCAPath, | ||||
| 	}) | ||||
| 	config.Vault.CAPath = c.flagCAPath | ||||
| 	c.setStringFlag(f, config.Vault.ClientCert, &StringVar{ | ||||
| 		Name:    flagNameClientCert, | ||||
| 		Target:  &c.flagClientCert, | ||||
| 		Default: "", | ||||
| 		EnvVar:  api.EnvVaultClientCert, | ||||
| 	}) | ||||
| 	config.Vault.ClientCert = c.flagClientCert | ||||
| 	c.setStringFlag(f, config.Vault.ClientKey, &StringVar{ | ||||
| 		Name:    flagNameClientKey, | ||||
| 		Target:  &c.flagClientKey, | ||||
| 		Default: "", | ||||
| 		EnvVar:  api.EnvVaultClientKey, | ||||
| 	}) | ||||
| 	config.Vault.ClientKey = c.flagClientKey | ||||
| 	c.setBoolFlag(f, config.Vault.TLSSkipVerify, &BoolVar{ | ||||
| 		Name:    flagNameTLSSkipVerify, | ||||
| 		Target:  &c.flagTLSSkipVerify, | ||||
| 		Default: false, | ||||
| 		EnvVar:  api.EnvVaultSkipVerify, | ||||
| 	}) | ||||
| 	config.Vault.TLSSkipVerify = c.flagTLSSkipVerify | ||||
| 	c.setStringFlag(f, config.Vault.TLSServerName, &StringVar{ | ||||
| 		Name:    flagTLSServerName, | ||||
| 		Target:  &c.flagTLSServerName, | ||||
| 		Default: "", | ||||
| 		EnvVar:  api.EnvVaultTLSServerName, | ||||
| 	}) | ||||
| 	config.Vault.TLSServerName = c.flagTLSServerName | ||||
|  | ||||
| 	return config | ||||
| } | ||||
|  | ||||
| // verifyRequestHeader wraps an http.Handler inside a Handler that checks for | ||||
| // the request header that is used for SSRF protection. | ||||
| func verifyRequestHeader(handler http.Handler) http.Handler { | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| @@ -38,6 +37,7 @@ type Config struct { | ||||
| 	DisableKeepAlivesCaching    bool                       `hcl:"-"` | ||||
| 	DisableKeepAlivesTemplating bool                       `hcl:"-"` | ||||
| 	DisableKeepAlivesAutoAuth   bool                       `hcl:"-"` | ||||
| 	LogFile                     string                     `hcl:"log_file"` | ||||
| } | ||||
|  | ||||
| const ( | ||||
| @@ -173,7 +173,7 @@ func LoadConfig(path string) (*Config, error) { | ||||
| 	} | ||||
|  | ||||
| 	// Read the file | ||||
| 	d, err := ioutil.ReadFile(path) | ||||
| 	d, err := os.ReadFile(path) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|   | ||||
| @@ -230,6 +230,7 @@ func TestLoadConfigFile(t *testing.T) { | ||||
| 				NumRetries: 12, | ||||
| 			}, | ||||
| 		}, | ||||
| 		LogFile: "/var/log/vault/vault-agent.log", | ||||
| 	} | ||||
|  | ||||
| 	config.Prune() | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| pid_file = "./pidfile" | ||||
| log_file = "/var/log/vault/vault-agent.log" | ||||
|  | ||||
| auto_auth { | ||||
| 	method "aws" { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| pid_file = "./pidfile" | ||||
| log_file = "/var/log/vault/vault-agent.log" | ||||
|  | ||||
| auto_auth { | ||||
| 	method { | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import ( | ||||
| 	"github.com/hashicorp/vault/api" | ||||
| 	credAppRole "github.com/hashicorp/vault/builtin/credential/approle" | ||||
| 	"github.com/hashicorp/vault/command/agent" | ||||
| 	agentConfig "github.com/hashicorp/vault/command/agent/config" | ||||
| 	vaulthttp "github.com/hashicorp/vault/http" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/consts" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/logging" | ||||
| @@ -27,9 +28,26 @@ import ( | ||||
| 	"github.com/hashicorp/vault/sdk/logical" | ||||
| 	"github.com/hashicorp/vault/vault" | ||||
| 	"github.com/mitchellh/cli" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	BasicHclConfig = ` | ||||
| log_file = "/foo/bar/juan.log" | ||||
| vault { | ||||
| 	address = "http://127.0.0.1:8200" | ||||
| 	retry { | ||||
| 		num_retries = 5 | ||||
| 	} | ||||
| } | ||||
|  | ||||
| listener "tcp" { | ||||
| 	address = "127.0.0.1:8100" | ||||
| 	tls_disable = true | ||||
| }` | ||||
| ) | ||||
|  | ||||
| func testAgentCommand(tb testing.TB, logger hclog.Logger) (*cli.MockUi, *AgentCommand) { | ||||
| 	tb.Helper() | ||||
|  | ||||
| @@ -1245,6 +1263,27 @@ func makeTempFile(t *testing.T, name, contents string) string { | ||||
| 	return path | ||||
| } | ||||
|  | ||||
| func populateTempFile(t *testing.T, name, contents string) *os.File { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	file, err := os.CreateTemp(t.TempDir(), name) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	_, err = file.WriteString(contents) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	err = file.Close() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	return file | ||||
| } | ||||
|  | ||||
| // handler makes 500 errors happen for reads on /v1/secret. | ||||
| // Definitely not thread-safe, do not use t.Parallel with this. | ||||
| type handler struct { | ||||
| @@ -2211,6 +2250,105 @@ cache {} | ||||
| 	wg.Wait() | ||||
| } | ||||
|  | ||||
| func TestAgent_LogFile_EnvVarOverridesConfig(t *testing.T) { | ||||
| 	// Create basic config | ||||
| 	configFile := populateTempFile(t, "agent-config.hcl", BasicHclConfig) | ||||
| 	cfg, err := agentConfig.LoadConfig(configFile.Name()) | ||||
| 	if err != nil { | ||||
| 		t.Fatal("Cannot load config to test update/merge", err) | ||||
| 	} | ||||
|  | ||||
| 	// Sanity check that the config value is the current value | ||||
| 	assert.Equal(t, "/foo/bar/juan.log", cfg.LogFile) | ||||
|  | ||||
| 	// Make sure the env var is configured | ||||
| 	oldEnvVarLogFile := os.Getenv(EnvVaultLogFile) | ||||
| 	os.Setenv(EnvVaultLogFile, "/squiggle/logs.txt") | ||||
| 	if oldEnvVarLogFile == "" { | ||||
| 		defer os.Unsetenv(EnvVaultLogFile) | ||||
| 	} else { | ||||
| 		defer os.Setenv(EnvVaultLogFile, oldEnvVarLogFile) | ||||
| 	} | ||||
|  | ||||
| 	// Initialize the command and parse any flags | ||||
| 	cmd := &AgentCommand{BaseCommand: &BaseCommand{}} | ||||
| 	f := cmd.Flags() | ||||
| 	err = f.Parse([]string{}) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	// Update the config based on the inputs. | ||||
| 	cfg = cmd.aggregateConfig(f, cfg) | ||||
|  | ||||
| 	assert.NotEqual(t, "/foo/bar/juan.log", cfg.LogFile) | ||||
| 	assert.Equal(t, "/squiggle/logs.txt", cfg.LogFile) | ||||
| } | ||||
|  | ||||
| func TestAgent_LogFile_CliOverridesEnvVar(t *testing.T) { | ||||
| 	// Create basic config | ||||
| 	configFile := populateTempFile(t, "agent-config.hcl", BasicHclConfig) | ||||
| 	cfg, err := agentConfig.LoadConfig(configFile.Name()) | ||||
| 	if err != nil { | ||||
| 		t.Fatal("Cannot load config to test update/merge", err) | ||||
| 	} | ||||
|  | ||||
| 	// Sanity check that the config value is the current value | ||||
| 	assert.Equal(t, "/foo/bar/juan.log", cfg.LogFile) | ||||
|  | ||||
| 	// Make sure the env var is configured | ||||
| 	oldEnvVarLogFile := os.Getenv(EnvVaultLogFile) | ||||
| 	os.Setenv(EnvVaultLogFile, "/squiggle/logs.txt") | ||||
| 	if oldEnvVarLogFile == "" { | ||||
| 		defer os.Unsetenv(EnvVaultLogFile) | ||||
| 	} else { | ||||
| 		defer os.Setenv(EnvVaultLogFile, oldEnvVarLogFile) | ||||
| 	} | ||||
|  | ||||
| 	// Initialize the command and parse any flags | ||||
| 	cmd := &AgentCommand{BaseCommand: &BaseCommand{}} | ||||
| 	f := cmd.Flags() | ||||
| 	// Simulate the flag being specified | ||||
| 	err = f.Parse([]string{"-log-file=/foo/bar/test.log"}) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	// Update the config based on the inputs. | ||||
| 	cfg = cmd.aggregateConfig(f, cfg) | ||||
|  | ||||
| 	assert.NotEqual(t, "/foo/bar/juan.log", cfg.LogFile) | ||||
| 	assert.NotEqual(t, "/squiggle/logs.txt", cfg.LogFile) | ||||
| 	assert.Equal(t, "/foo/bar/test.log", cfg.LogFile) | ||||
| } | ||||
|  | ||||
| func TestAgent_LogFile_Config(t *testing.T) { | ||||
| 	// Sanity check, remove any env var | ||||
| 	os.Unsetenv(EnvVaultLogFile) | ||||
|  | ||||
| 	configFile := populateTempFile(t, "agent-config.hcl", BasicHclConfig) | ||||
|  | ||||
| 	cfg, err := agentConfig.LoadConfig(configFile.Name()) | ||||
| 	if err != nil { | ||||
| 		t.Fatal("Cannot load config to test update/merge", err) | ||||
| 	} | ||||
|  | ||||
| 	// Sanity check that the config value is the current value | ||||
| 	assert.Equal(t, "/foo/bar/juan.log", cfg.LogFile, "sanity check on log config failed") | ||||
|  | ||||
| 	// Parse the cli flags (but we pass in an empty slice) | ||||
| 	cmd := &AgentCommand{BaseCommand: &BaseCommand{}} | ||||
| 	f := cmd.Flags() | ||||
| 	err = f.Parse([]string{}) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	cfg = cmd.aggregateConfig(f, cfg) | ||||
|  | ||||
| 	assert.Equal(t, "/foo/bar/juan.log", cfg.LogFile, "actual config check") | ||||
| } | ||||
|  | ||||
| // Get a randomly assigned port and then free it again before returning it. | ||||
| // There is still a race when trying to use it, but should work better | ||||
| // than a static port. | ||||
|   | ||||
| @@ -82,6 +82,11 @@ const ( | ||||
| 	EnvVaultLicensePath = "VAULT_LICENSE_PATH" | ||||
| 	// EnvVaultDetailed is to output detailed information (e.g., ListResponseWithInfo). | ||||
| 	EnvVaultDetailed = `VAULT_DETAILED` | ||||
| 	// EnvVaultLogFile is used to specify the path to the log file that Vault should use for logging | ||||
| 	EnvVaultLogFile = "VAULT_LOG_FILE" | ||||
| 	// EnvVaultLogLevel is used to specify the log level applied to logging | ||||
| 	// Supported log levels: Trace, Debug, Error, Warn, Info | ||||
| 	EnvVaultLogLevel = "VAULT_LOG_LEVEL" | ||||
|  | ||||
| 	// DisableSSCTokens is an env var used to disable index bearing | ||||
| 	// token functionality | ||||
| @@ -136,6 +141,11 @@ const ( | ||||
| 	flagNameUserLockoutDisable = "user-lockout-disable" | ||||
| 	// flagNameDisableRedirects is used to prevent the client from honoring a single redirect as a response to a request | ||||
| 	flagNameDisableRedirects = "disable-redirects" | ||||
| 	// flagNameLogFile is used to specify the path to the log file that Vault should use for logging | ||||
| 	flagNameLogFile = "log-file" | ||||
| 	// flagNameLogLevel is used to specify the log level applied to logging | ||||
| 	// Supported log levels: Trace, Debug, Error, Warn, Info | ||||
| 	flagNameLogLevel = "log-level" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
|   | ||||
							
								
								
									
										56
									
								
								helper/logging/logfile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								helper/logging/logfile.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| package logging | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| ) | ||||
|  | ||||
| type LogFile struct { | ||||
| 	// Name of the log file | ||||
| 	fileName string | ||||
|  | ||||
| 	// Path to the log file | ||||
| 	logPath string | ||||
|  | ||||
| 	// fileInfo is the pointer to the current file being written to | ||||
| 	fileInfo *os.File | ||||
|  | ||||
| 	// acquire is the mutex utilized to ensure we have no concurrency issues | ||||
| 	acquire sync.Mutex | ||||
| } | ||||
|  | ||||
| func NewLogFile(logPath string, fileName string) *LogFile { | ||||
| 	return &LogFile{ | ||||
| 		fileName: strings.TrimSpace(fileName), | ||||
| 		logPath:  strings.TrimSpace(logPath), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Write is used to implement io.Writer | ||||
| func (l *LogFile) Write(b []byte) (n int, err error) { | ||||
| 	l.acquire.Lock() | ||||
| 	defer l.acquire.Unlock() | ||||
| 	// Create a new file if we have no file to write to | ||||
| 	if l.fileInfo == nil { | ||||
| 		if err := l.openNew(); err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return l.fileInfo.Write(b) | ||||
| } | ||||
|  | ||||
| func (l *LogFile) openNew() error { | ||||
| 	newFilePath := filepath.Join(l.logPath, l.fileName) | ||||
|  | ||||
| 	// Try to open an existing file or create a new one if it doesn't exist. | ||||
| 	filePointer, err := os.OpenFile(newFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o640) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	l.fileInfo = filePointer | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										22
									
								
								helper/logging/logfile_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								helper/logging/logfile_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| package logging | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func TestLogFile_openNew(t *testing.T) { | ||||
| 	logFile := NewLogFile(t.TempDir(), "vault-agent.log") | ||||
| 	err := logFile.openNew() | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	msg := "[INFO] Something" | ||||
| 	_, err = logFile.Write([]byte(msg)) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	content, err := os.ReadFile(logFile.fileInfo.Name()) | ||||
| 	require.NoError(t, err) | ||||
| 	require.Contains(t, string(content), msg) | ||||
| } | ||||
							
								
								
									
										135
									
								
								helper/logging/logger.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								helper/logging/logger.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| package logging | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	log "github.com/hashicorp/go-hclog" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	UnspecifiedFormat LogFormat = iota | ||||
| 	StandardFormat | ||||
| 	JSONFormat | ||||
| ) | ||||
|  | ||||
| type LogFormat int | ||||
|  | ||||
| // LogConfig should be used to supply configuration when creating a new Vault logger | ||||
| type LogConfig struct { | ||||
| 	name        string | ||||
| 	logLevel    log.Level | ||||
| 	logFormat   LogFormat | ||||
| 	logFilePath string | ||||
| } | ||||
|  | ||||
| func NewLogConfig(name string, logLevel log.Level, logFormat LogFormat, logFilePath string) LogConfig { | ||||
| 	return LogConfig{ | ||||
| 		name:        name, | ||||
| 		logLevel:    logLevel, | ||||
| 		logFormat:   logFormat, | ||||
| 		logFilePath: strings.TrimSpace(logFilePath), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c LogConfig) IsFormatJson() bool { | ||||
| 	return c.logFormat == JSONFormat | ||||
| } | ||||
|  | ||||
| // Stringer implementation | ||||
| func (lf LogFormat) String() string { | ||||
| 	switch lf { | ||||
| 	case UnspecifiedFormat: | ||||
| 		return "unspecified" | ||||
| 	case StandardFormat: | ||||
| 		return "standard" | ||||
| 	case JSONFormat: | ||||
| 		return "json" | ||||
| 	} | ||||
|  | ||||
| 	// unreachable | ||||
| 	return "unknown" | ||||
| } | ||||
|  | ||||
| // noErrorWriter is a wrapper to suppress errors when writing to w. | ||||
| type noErrorWriter struct { | ||||
| 	w io.Writer | ||||
| } | ||||
|  | ||||
| func (w noErrorWriter) Write(p []byte) (n int, err error) { | ||||
| 	_, _ = w.w.Write(p) | ||||
| 	// We purposely return n == len(p) as if write was successful | ||||
| 	return len(p), nil | ||||
| } | ||||
|  | ||||
| // Setup creates a new logger with the specified configuration and writer | ||||
| func Setup(config LogConfig, w io.Writer) (log.InterceptLogger, error) { | ||||
| 	// Validate the log level | ||||
| 	if config.logLevel.String() == "unknown" { | ||||
| 		return nil, fmt.Errorf("invalid log level: %v", config.logLevel) | ||||
| 	} | ||||
|  | ||||
| 	// If out is os.Stdout and Vault is being run as a Windows Service, writes will | ||||
| 	// fail silently, which may inadvertently prevent writes to other writers. | ||||
| 	// noErrorWriter is used as a wrapper to suppress any errors when writing to out. | ||||
| 	writers := []io.Writer{noErrorWriter{w: w}} | ||||
|  | ||||
| 	if config.logFilePath != "" { | ||||
| 		dir, fileName := filepath.Split(config.logFilePath) | ||||
| 		if fileName == "" { | ||||
| 			fileName = "vault-agent.log" | ||||
| 		} | ||||
| 		logFile := NewLogFile(dir, fileName) | ||||
| 		if err := logFile.openNew(); err != nil { | ||||
| 			return nil, fmt.Errorf("failed to set up file logging: %w", err) | ||||
| 		} | ||||
| 		writers = append(writers, logFile) | ||||
| 	} | ||||
|  | ||||
| 	logger := log.NewInterceptLogger(&log.LoggerOptions{ | ||||
| 		Name:       config.name, | ||||
| 		Level:      config.logLevel, | ||||
| 		Output:     io.MultiWriter(writers...), | ||||
| 		JSONFormat: config.IsFormatJson(), | ||||
| 	}) | ||||
| 	return logger, nil | ||||
| } | ||||
|  | ||||
| // ParseLogFormat parses the log format from the provided string. | ||||
| func ParseLogFormat(format string) (LogFormat, error) { | ||||
| 	switch strings.ToLower(strings.TrimSpace(format)) { | ||||
| 	case "": | ||||
| 		return UnspecifiedFormat, nil | ||||
| 	case "standard": | ||||
| 		return StandardFormat, nil | ||||
| 	case "json": | ||||
| 		return JSONFormat, nil | ||||
| 	default: | ||||
| 		return UnspecifiedFormat, fmt.Errorf("unknown log format: %s", format) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ParseLogLevel(logLevel string) (log.Level, error) { | ||||
| 	var result log.Level | ||||
| 	logLevel = strings.ToLower(strings.TrimSpace(logLevel)) | ||||
|  | ||||
| 	switch logLevel { | ||||
| 	case "trace": | ||||
| 		result = log.Trace | ||||
| 	case "debug": | ||||
| 		result = log.Debug | ||||
| 	case "notice", "info", "": | ||||
| 		result = log.Info | ||||
| 	case "warn", "warning": | ||||
| 		result = log.Warn | ||||
| 	case "err", "error": | ||||
| 		result = log.Error | ||||
| 	default: | ||||
| 		return -1, errors.New(fmt.Sprintf("unknown log level: %s", logLevel)) | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
							
								
								
									
										128
									
								
								helper/logging/logger_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								helper/logging/logger_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| package logging | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"testing" | ||||
|  | ||||
| 	log "github.com/hashicorp/go-hclog" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func TestLogger_SetupBasic(t *testing.T) { | ||||
| 	cfg := NewLogConfig("test-system", log.Info, StandardFormat, t.TempDir()+"test.log") | ||||
|  | ||||
| 	logger, err := Setup(cfg, nil) | ||||
| 	require.NoError(t, err) | ||||
| 	require.NotNil(t, logger) | ||||
| } | ||||
|  | ||||
| func TestLogger_SetupInvalidLogLevel(t *testing.T) { | ||||
| 	cfg := NewLogConfig("test-system", 999, StandardFormat, t.TempDir()+"test.log") | ||||
|  | ||||
| 	_, err := Setup(cfg, nil) | ||||
| 	assert.Containsf(t, err.Error(), "invalid log level", "expected error %s", err) | ||||
| } | ||||
|  | ||||
| func TestLogger_SetupLoggerErrorLevel(t *testing.T) { | ||||
| 	cfg := NewLogConfig("test-system", log.Error, StandardFormat, t.TempDir()+"test.log") | ||||
| 	var buf bytes.Buffer | ||||
|  | ||||
| 	logger, err := Setup(cfg, &buf) | ||||
| 	require.NoError(t, err) | ||||
| 	require.NotNil(t, logger) | ||||
|  | ||||
| 	logger.Error("test error msg") | ||||
| 	logger.Info("test info msg") | ||||
|  | ||||
| 	output := buf.String() | ||||
|  | ||||
| 	require.Contains(t, output, "[ERROR] test-system: test error msg") | ||||
| 	require.NotContains(t, output, "[INFO] test-system: test info msg") | ||||
| } | ||||
|  | ||||
| func TestLogger_SetupLoggerDebugLevel(t *testing.T) { | ||||
| 	cfg := NewLogConfig("test-system", log.Debug, StandardFormat, t.TempDir()+"test.log") | ||||
| 	var buf bytes.Buffer | ||||
|  | ||||
| 	logger, err := Setup(cfg, &buf) | ||||
| 	require.NoError(t, err) | ||||
| 	require.NotNil(t, logger) | ||||
|  | ||||
| 	logger.Info("test info msg") | ||||
| 	logger.Debug("test debug msg") | ||||
|  | ||||
| 	output := buf.String() | ||||
|  | ||||
| 	require.Contains(t, output, "[INFO]  test-system: test info msg") | ||||
| 	require.Contains(t, output, "[DEBUG] test-system: test debug msg") | ||||
| } | ||||
|  | ||||
| func TestLogger_SetupLoggerWithName(t *testing.T) { | ||||
| 	cfg := NewLogConfig("test-system", log.Debug, StandardFormat, t.TempDir()+"test.log") | ||||
| 	var buf bytes.Buffer | ||||
|  | ||||
| 	logger, err := Setup(cfg, &buf) | ||||
| 	require.NoError(t, err) | ||||
| 	require.NotNil(t, logger) | ||||
|  | ||||
| 	logger.Warn("test warn msg") | ||||
|  | ||||
| 	require.Contains(t, buf.String(), "[WARN]  test-system: test warn msg") | ||||
| } | ||||
|  | ||||
| func TestLogger_SetupLoggerWithJSON(t *testing.T) { | ||||
| 	cfg := NewLogConfig("test-system", log.Debug, JSONFormat, t.TempDir()+"test.log") | ||||
| 	var buf bytes.Buffer | ||||
|  | ||||
| 	logger, err := Setup(cfg, &buf) | ||||
| 	require.NoError(t, err) | ||||
| 	require.NotNil(t, logger) | ||||
|  | ||||
| 	logger.Warn("test warn msg") | ||||
|  | ||||
| 	var jsonOutput map[string]string | ||||
| 	err = json.Unmarshal(buf.Bytes(), &jsonOutput) | ||||
| 	require.NoError(t, err) | ||||
| 	require.Contains(t, jsonOutput, "@level") | ||||
| 	require.Equal(t, jsonOutput["@level"], "warn") | ||||
| 	require.Contains(t, jsonOutput, "@message") | ||||
| 	require.Equal(t, jsonOutput["@message"], "test warn msg") | ||||
| } | ||||
|  | ||||
| func TestLogger_SetupLoggerWithValidLogPath(t *testing.T) { | ||||
| 	tmpDir := t.TempDir() | ||||
| 	cfg := NewLogConfig("test-system", log.Info, StandardFormat, tmpDir+"/") | ||||
| 	var buf bytes.Buffer | ||||
|  | ||||
| 	logger, err := Setup(cfg, &buf) | ||||
| 	require.NoError(t, err) | ||||
| 	require.NotNil(t, logger) | ||||
| } | ||||
|  | ||||
| func TestLogger_SetupLoggerWithInValidLogPath(t *testing.T) { | ||||
| 	cfg := NewLogConfig("test-system", log.Info, StandardFormat, "nonexistentdir/") | ||||
| 	var buf bytes.Buffer | ||||
|  | ||||
| 	logger, err := Setup(cfg, &buf) | ||||
| 	require.Error(t, err) | ||||
| 	require.True(t, errors.Is(err, os.ErrNotExist)) | ||||
| 	require.Nil(t, logger) | ||||
| } | ||||
|  | ||||
| func TestLogger_SetupLoggerWithInValidLogPathPermission(t *testing.T) { | ||||
| 	tmpDir := "/tmp/" + t.Name() | ||||
|  | ||||
| 	os.Mkdir(tmpDir, 0o000) | ||||
| 	defer os.RemoveAll(tmpDir) | ||||
| 	cfg := NewLogConfig("test-system", log.Info, StandardFormat, tmpDir+"/") | ||||
| 	var buf bytes.Buffer | ||||
|  | ||||
| 	logger, err := Setup(cfg, &buf) | ||||
| 	require.Error(t, err) | ||||
| 	require.True(t, errors.Is(err, os.ErrPermission)) | ||||
| 	require.Nil(t, logger) | ||||
| } | ||||
| @@ -127,6 +127,12 @@ See the [caching](/docs/agent/caching#api) page for details on the cache API. | ||||
|  | ||||
| ## Configuration | ||||
|  | ||||
| ### Command Options | ||||
|  | ||||
| - `-log-file` `(string: "")` - If specified, should contain the full file path to use for outputting log files from Vault. | ||||
|  | ||||
| ### Configuration File Options | ||||
|  | ||||
| These are the currently-available general configuration option: | ||||
|  | ||||
| - `vault` <code>([vault][vault]: <optional\>)</code> - Specifies the remote Vault server the Agent connects to. | ||||
| @@ -373,4 +379,4 @@ template { | ||||
| [listener]: /docs/agent#listener-stanza | ||||
| [listener_main]: /docs/configuration/listener/tcp | ||||
| [winsvc]: /docs/agent/winsvc | ||||
| [telemetry]: /docs/configuration/telemetry | ||||
| [telemetry]: /docs/configuration/telemetry | ||||
| @@ -35,7 +35,7 @@ of Vault Agent as a service, using "Vault Agent" as the display name, and starti | ||||
| The `binPath` argument should include the fully qualified path to the Vault executable, as well as any arguments required. | ||||
|  | ||||
| ```shell-session | ||||
| PS C:\Windows\system32> sc.exe create VaultAgent binPath= "C:\vault\vault.exe agent -config=C:\vault\agent-config.hcl" displayName= "Vault Agent" start= auto | ||||
| PS C:\Windows\system32> sc.exe create VaultAgent binPath="C:\vault\vault.exe agent -config=C:\vault\agent-config.hcl" displayName="Vault Agent" start=auto | ||||
| [SC] CreateService SUCCESS | ||||
| ``` | ||||
|  | ||||
|   | ||||
| @@ -331,6 +331,10 @@ precedence over [#VAULT_LICENSE_PATH](#vault_license_path) and | ||||
| [Enterprise, Server only] Specify a path to a license on disk to use for this node. | ||||
| This takes precedence over [license_path in config](/docs/configuration#license_path). | ||||
|  | ||||
| ### `VAULT_LOG_FILE` | ||||
|  | ||||
| (Agent only) If provided, specifies the full path to a log file Vault should use to write its logs. | ||||
|  | ||||
| ### `VAULT_MAX_RETRIES` | ||||
|  | ||||
| Maximum number of retries when certain error codes are encountered. The default | ||||
| @@ -434,4 +438,4 @@ list of available flags, run: | ||||
|  | ||||
| ```shell-session | ||||
| $ vault <subcommand> -h | ||||
| ``` | ||||
| ``` | ||||
		Reference in New Issue
	
	Block a user
	 Peter Wilson
					Peter Wilson