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:
parent
a6e11bd170
commit
7ae65df94e
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
|
||||
```
|
||||
```
|
Loading…
Reference in New Issue
Block a user