Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix non-JSON log messages when using -log-format JSON #24252

Merged
merged 12 commits into from
Nov 29, 2023
3 changes: 3 additions & 0 deletions changelog/24252.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
agent/logging: Agent should now honor correct -log-format and -log-file settings in logs generated by the consul-template library.
```
35 changes: 22 additions & 13 deletions command/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (

systemd "github.com/coreos/go-systemd/daemon"
ctconfig "github.com/hashicorp/consul-template/config"
log "github.com/hashicorp/go-hclog"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-secure-stdlib/gatedwriter"
"github.com/hashicorp/go-secure-stdlib/parseutil"
Expand Down Expand Up @@ -78,7 +78,7 @@ type AgentCommand struct {

logWriter io.Writer
logGate *gatedwriter.Writer
logger log.Logger
logger hclog.Logger

// Telemetry object
metricsHelper *metricsutil.MetricsHelper
Expand Down Expand Up @@ -210,7 +210,16 @@ func (c *AgentCommand) Run(args []string) int {
c.outputErrors(err)
return 1
}

// Update the logger and then base the log writer on that logger.
// Log writer is supplied to consul-template runners for templates and execs.
// We want to ensure that consul-template will honor the settings, for example
// if the -log-format is JSON we want JSON, not a mix of JSON and non-JSON messages.
c.logger = l
c.logWriter = l.StandardWriter(&hclog.StandardLoggerOptions{
InferLevels: true,
InferLevelsWithTimestamp: true,
})
Comment on lines +219 to +222
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the actual bug fix.


infoKeys := make([]string, 0, 10)
info := make(map[string]string)
Expand Down Expand Up @@ -1093,31 +1102,31 @@ func (c *AgentCommand) handleQuit(enabled bool) http.Handler {
}

// newLogger creates a logger based on parsed config field on the Agent Command struct.
func (c *AgentCommand) newLogger() (log.InterceptLogger, error) {
func (c *AgentCommand) newLogger() (hclog.InterceptLogger, error) {
if c.config == nil {
return nil, fmt.Errorf("cannot create logger, no config")
}

var errors error
var errs *multierror.Error

// Parse all the log related config
logLevel, err := logging.ParseLogLevel(c.config.LogLevel)
if err != nil {
errors = multierror.Append(errors, err)
errs = multierror.Append(errs, err)
}

logFormat, err := logging.ParseLogFormat(c.config.LogFormat)
if err != nil {
errors = multierror.Append(errors, err)
errs = multierror.Append(errs, err)
}

logRotateDuration, err := parseutil.ParseDurationSecond(c.config.LogRotateDuration)
if err != nil {
errors = multierror.Append(errors, err)
errs = multierror.Append(errs, err)
}

if errors != nil {
return nil, errors
if errs != nil {
return nil, errs
}

logCfg := &logging.LogConfig{
Expand All @@ -1140,20 +1149,20 @@ func (c *AgentCommand) newLogger() (log.InterceptLogger, error) {

// loadConfig attempts to generate an Agent config from the file(s) specified.
func (c *AgentCommand) loadConfig(paths []string) (*agentConfig.Config, error) {
var errors error
var errs *multierror.Error
cfg := agentConfig.NewConfig()

for _, configPath := range paths {
configFromPath, err := agentConfig.LoadConfig(configPath)
if err != nil {
errors = multierror.Append(errors, fmt.Errorf("error loading configuration from %s: %w", configPath, err))
errs = multierror.Append(errs, fmt.Errorf("error loading configuration from %s: %w", configPath, err))
} else {
cfg = cfg.Merge(configFromPath)
}
}

if errors != nil {
return nil, errors
if errs != nil {
return nil, errs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the error is of type multierror.Error, you may want to use errs.ErrorOrNil() on the return.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @hghaf099, do you mean like this?

if errs.ErrorOrNil() != nil {
		return nil, errs.ErrorOrNil()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, my bad. It seems that we only need to return if the error is non-nil. Then what you already had is sufficient. Sorry for the confusion.

}

if err := cfg.ValidateConfig(); err != nil {
Expand Down
1 change: 0 additions & 1 deletion command/agent/internal/ctmanager/runner_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
ctconfig "github.com/hashicorp/consul-template/config"
ctlogging "github.com/hashicorp/consul-template/logging"
"github.com/hashicorp/go-hclog"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nobody likes the extra line.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"github.com/hashicorp/vault/command/agent/config"
"github.com/hashicorp/vault/sdk/helper/pointerutil"
)
Expand Down
104 changes: 103 additions & 1 deletion command/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package command

import (
"bufio"
"crypto/tls"
"crypto/x509"
"encoding/json"
Expand Down Expand Up @@ -826,7 +827,7 @@ auto_auth {
verify := func(suffix string) {
t.Helper()
// We need to poll for a bit to give Agent time to render the
// templates. Without this this, the test will attempt to read
// templates. Without this, the test will attempt to read
// the temp dir before Agent has had time to render and will
// likely fail the test
tick := time.Tick(1 * time.Second)
Expand Down Expand Up @@ -3070,6 +3071,107 @@ vault {
}
}

// TestAgent_Logging_ConsulTemplate attempts to ensure two things about Vault Agent logs:
// 1. When -log-format command line arg is set to JSON, it is honored as the output format
// for messages generated from within the consul-template library.
// 2. When -log-file command line arg is supplied, a file receives all log messages
// generated by the consul-template library (they don't just go to stdout/stderr).
// Should prevent a regression of: https://github.com/hashicorp/vault/issues/21109
func TestAgent_Logging_ConsulTemplate(t *testing.T) {
const (
runnerLogMessage = "(runner) creating new runner (dry: false, once: false)"
)

// Configure a Vault server so Agent can successfully communicate and render its templates
cluster := minimal.NewTestSoloCluster(t, nil)
apiClient := cluster.Cores[0].Client
t.Setenv(api.EnvVaultAddress, apiClient.Address())
tempDir := t.TempDir()
roleIDPath, secretIDPath := setupAppRoleAndKVMounts(t, apiClient)

// Create relevant configs for Vault Agent (config, template config)
templateSrc := filepath.Join(tempDir, "render_1.tmpl")
err := os.WriteFile(templateSrc, []byte(templateContents(1)), 0o600)
require.NoError(t, err)
templateConfig := fmt.Sprintf(templateConfigString, templateSrc, tempDir, "render_1.json")

config := `
vault {
address = "%s"
tls_skip_verify = true
}

auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "%s"
secret_id_file_path = "%s"
remove_secret_id_file_after_reading = false
}
}
}

%s
`
config = fmt.Sprintf(config, apiClient.Address(), roleIDPath, secretIDPath, templateConfig)
configFileName := filepath.Join(tempDir, "config.hcl")
err = os.WriteFile(configFileName, []byte(config), 0o600)
require.NoError(t, err)
_, cmd := testAgentCommand(t, nil)
logFilePath := filepath.Join(tempDir, "agent")

// Start Vault Agent
go func() {
code := cmd.Run([]string{"-config", configFileName, "-log-format", "json", "-log-file", logFilePath, "-log-level", "trace"})
require.Equalf(t, 0, code, "Vault Agent returned a non-zero exit code")
}()

select {
case <-cmd.startedCh:
case <-time.After(5 * time.Second):
t.Fatal("timeout starting agent")
}

// Give Vault Agent some time to render our template.
time.Sleep(3 * time.Second)

// This flag will be used to capture whether we saw a consul-template log
// message in the log file (the presence of the log file is also part of the test)
found := false

// Vault Agent file logs will match agent-{timestamp}.log based on the
// cmd line argument we supplied, e.g. agent-1701258869573205000.log
m, err := filepath.Glob(logFilePath + "*")
require.NoError(t, err)
require.Truef(t, len(m) > 0, "no files were found")

for _, p := range m {
f, err := os.Open(p)
require.NoError(t, err)

fs := bufio.NewScanner(f)
fs.Split(bufio.ScanLines)

for fs.Scan() {
s := fs.Text()
entry := make(map[string]string)
err := json.Unmarshal([]byte(s), &entry)
require.NoError(t, err)
v, ok := entry["@message"]
if !ok {
continue
}
if v == runnerLogMessage {
found = true
break
}
}
}

require.Truef(t, found, "unable to find consul-template partial message in logs", runnerLogMessage)
}

// 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.
Expand Down