Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"

log "github.com/cloudposse/atmos/pkg/logger"
)

const (
Expand All @@ -25,10 +27,18 @@ func init() {
authCmd.PersistentFlags().String(ProfileFlagName, "", "Specify the profile to use for authentication.")
authCmd.PersistentFlags().StringP(IdentityFlagName, "i", "", "Specify the target identity to assume.")
// Bind to Viper and env (flags > env > config > defaults).
_ = viper.BindEnv(ProfileFlagName, "ATMOS_PROFILE", "PROFILE")
_ = viper.BindEnv(IdentityFlagName, "ATMOS_IDENTITY", "IDENTITY")
_ = viper.BindPFlag(ProfileFlagName, authCmd.PersistentFlags().Lookup(ProfileFlagName))
_ = viper.BindPFlag(IdentityFlagName, authCmd.PersistentFlags().Lookup(IdentityFlagName))
if err := viper.BindEnv(ProfileFlagName, "ATMOS_PROFILE", "PROFILE"); err != nil {
log.Trace("Failed to bind profile environment variables", "error", err)
}
if err := viper.BindEnv(IdentityFlagName, "ATMOS_IDENTITY", "IDENTITY"); err != nil {
log.Trace("Failed to bind identity environment variables", "error", err)
}
if err := viper.BindPFlag(ProfileFlagName, authCmd.PersistentFlags().Lookup(ProfileFlagName)); err != nil {
log.Trace("Failed to bind profile flag", "error", err)
}
if err := viper.BindPFlag(IdentityFlagName, authCmd.PersistentFlags().Lookup(IdentityFlagName)); err != nil {
log.Trace("Failed to bind identity flag", "error", err)
}

RootCmd.AddCommand(authCmd)
}
19 changes: 14 additions & 5 deletions cmd/auth_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

errUtils "github.com/cloudposse/atmos/errors"
cfg "github.com/cloudposse/atmos/pkg/config"
log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/schema"
u "github.com/cloudposse/atmos/pkg/utils"
)
Expand Down Expand Up @@ -121,13 +122,21 @@ func outputEnvAsDotenv(envVars map[string]string) error {

func init() {
authEnvCmd.Flags().StringP("format", "f", "bash", "Output format: bash, json, dotenv.")
_ = viper.BindEnv("auth_env_format", "ATMOS_AUTH_ENV_FORMAT")
_ = viper.BindPFlag("auth_env_format", authEnvCmd.Flags().Lookup("format"))
_ = authEnvCmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if err := viper.BindEnv("auth_env_format", "ATMOS_AUTH_ENV_FORMAT"); err != nil {
log.Trace("Failed to bind auth_env_format environment variable", "error", err)
}
if err := viper.BindPFlag("auth_env_format", authEnvCmd.Flags().Lookup("format")); err != nil {
log.Trace("Failed to bind auth_env_format flag", "error", err)
}
if err := authEnvCmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return SupportedFormats, cobra.ShellCompDirectiveNoFileComp
})
}); err != nil {
log.Trace("Failed to register format flag completion", "error", err)
}

_ = viper.BindPFlag("identity", authCmd.PersistentFlags().Lookup("identity"))
if err := viper.BindPFlag("identity", authCmd.PersistentFlags().Lookup("identity")); err != nil {
log.Trace("Failed to bind identity flag", "error", err)
}
viper.MustBindEnv("identity", "ATMOS_IDENTITY")
authCmd.AddCommand(authEnvCmd)
}
9 changes: 7 additions & 2 deletions cmd/auth_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/cloudposse/atmos/pkg/auth/validation"
cfg "github.com/cloudposse/atmos/pkg/config"
log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/schema"
u "github.com/cloudposse/atmos/pkg/utils"
)
Expand Down Expand Up @@ -52,8 +53,12 @@ func executeAuthValidateCommand(cmd *cobra.Command, args []string) error {

func init() {
authValidateCmd.Flags().BoolP("verbose", "v", false, "Enable verbose output")
_ = viper.BindPFlag("auth.validate.verbose", authValidateCmd.Flags().Lookup("verbose"))
if err := viper.BindPFlag("auth.validate.verbose", authValidateCmd.Flags().Lookup("verbose")); err != nil {
log.Trace("Failed to bind auth.validate.verbose flag", "error", err)
}
viper.SetEnvPrefix("ATMOS")
_ = viper.BindEnv("auth.validate.verbose") // ATMOS_AUTH_VALIDATE_VERBOSE
if err := viper.BindEnv("auth.validate.verbose"); err != nil {
log.Trace("Failed to bind auth.validate.verbose environment variable", "error", err)
}
authCmd.AddCommand(authValidateCmd)
}
9 changes: 7 additions & 2 deletions cmd/auth_whoami.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
authTypes "github.com/cloudposse/atmos/pkg/auth/types"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/config/homedir"
log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/schema"
)

Expand Down Expand Up @@ -152,7 +153,11 @@ func sanitizeEnvMap(in map[string]string, homeDir string) map[string]string {

func init() {
authWhoamiCmd.Flags().StringP("output", "o", "", "Output format (json)")
_ = viper.BindPFlag("auth.whoami.output", authWhoamiCmd.Flags().Lookup("output"))
_ = viper.BindEnv("auth.whoami.output", "ATMOS_AUTH_WHOAMI_OUTPUT")
if err := viper.BindPFlag("auth.whoami.output", authWhoamiCmd.Flags().Lookup("output")); err != nil {
log.Trace("Failed to bind auth.whoami.output flag", "error", err)
}
if err := viper.BindEnv("auth.whoami.output", "ATMOS_AUTH_WHOAMI_OUTPUT"); err != nil {
log.Trace("Failed to bind auth.whoami.output environment variable", "error", err)
}
authCmd.AddCommand(authWhoamiCmd)
}
4 changes: 3 additions & 1 deletion cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,9 @@ func preCustomCommand(

// no "steps" means a sub command should be specified
if len(commandConfig.Steps) == 0 {
_ = cmd.Help()
if err := cmd.Help(); err != nil {
log.Trace("Failed to display command help", "error", err, "command", cmd.Name())
}
errUtils.Exit(0)
}
}
Expand Down
9 changes: 7 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,13 @@ func setupLogger(atmosConfig *schema.AtmosConfiguration) {
func cleanupLogFile() {
if logFileHandle != nil {
// Flush any remaining log data before closing.
_ = logFileHandle.Sync()
_ = logFileHandle.Close()
if err := logFileHandle.Sync(); err != nil {
// Don't use logger here as we're cleaning up the log file
fmt.Fprintf(os.Stderr, "Warning: failed to sync log file: %v\n", err)
}
if err := logFileHandle.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close log file: %v\n", err)
}
logFileHandle = nil
}
}
Expand Down
4 changes: 3 additions & 1 deletion cmd/validate_editorconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ func initializeConfig(cmd *cobra.Command) {
}
}

_ = currentConfig.Parse()
if err := currentConfig.Parse(); err != nil {
log.Trace("Failed to parse EditorConfig configuration", "error", err, "paths", configPaths)
}

if tmpExclude != "" {
currentConfig.Exclude = append(currentConfig.Exclude, tmpExclude)
Expand Down
111 changes: 110 additions & 1 deletion docs/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Atmos provides `utils.PrintfMessageToTUI` for writing TextUI messages. It always

### Trace

**Purpose**: Execution flow tracing and detailed debugging.
**Purpose**: Execution flow tracing, detailed debugging, and squelched error logging.

#### When to Use

Expand All @@ -29,6 +29,7 @@ Atmos provides `utils.PrintfMessageToTUI` for writing TextUI messages. It always
- Loop iterations in complex algorithms.
- Template expansion steps.
- Detailed state at each step of multi-stage operations.
- **Squelched errors** - errors that are intentionally ignored but should be recorded.

#### Characteristics

Expand All @@ -43,6 +44,9 @@ Atmos provides `utils.PrintfMessageToTUI` for writing TextUI messages. It always
- "Entering ProcessStackConfig with stack=dev, component=vpc"
- "Template evaluation: input={...}, context={...}, output={...}"
- "Cache lookup: key=stack-dev-vpc, found=true, age=2.3s"
- "Failed to close temporary file during cleanup" (squelched error)
- "Failed to remove temporary directory during cleanup" (squelched error)
- "Failed to bind environment variable" (squelched error)

---

Expand Down Expand Up @@ -183,6 +187,111 @@ Even these examples are borderline - they could arguably be Debug or Warning dep

---

## Squelched Errors

### What Are Squelched Errors?

Squelched errors are errors that are intentionally ignored because they don't affect the critical path of execution. Common examples include:
- Cleanup operations (removing temporary files, closing file handles)
- Non-critical configuration binding (environment variables, flags)
- Resource unlocking in defer statements
- UI rendering errors

### When to Squelch Errors

Errors should only be squelched when:
1. **The error is non-critical** - failure doesn't prevent the operation from succeeding
2. **Recovery is impossible** - we're in cleanup/defer code where we can't do anything about the error
3. **The operation is best-effort** - like optional environment variable binding

### The Golden Rule: Always Log Squelched Errors

Even when errors are intentionally ignored, they **must** be logged at Trace level. This ensures errors are never truly lost and can be investigated during debugging.

### Pattern: Squelched Error Logging

**Wrong**: Silent error squelching
```go
// ❌ WRONG: Error is completely lost
_ = os.Remove(tempFile)
_ = file.Close()
_ = viper.BindEnv("OPTIONAL_VAR", "VAR")
```

**Right**: Log squelched errors at Trace level
```go
// ✅ CORRECT: Cleanup with trace logging
if err := os.Remove(tempFile); err != nil && !os.IsNotExist(err) {
log.Trace("Failed to remove temporary file during cleanup", "error", err, "file", tempFile)
}

// ✅ CORRECT: Resource closing with trace logging
if err := file.Close(); err != nil {
log.Trace("Failed to close file", "error", err, "file", file.Name())
}

// ✅ CORRECT: Non-critical configuration with trace logging
if err := viper.BindEnv("OPTIONAL_VAR", "VAR"); err != nil {
log.Trace("Failed to bind environment variable", "error", err, "var", "OPTIONAL_VAR")
}
```

### Special Cases

#### Defer Statements
When squelching errors in defer statements, capture them in a closure:

```go
defer func() {
if err := lock.Unlock(); err != nil {
log.Trace("Failed to unlock", "error", err, "path", lockPath)
}
}()
```

#### File Existence Checks
When removing files, check for `os.IsNotExist` to avoid logging expected conditions:

```go
if err := os.Remove(tempFile); err != nil && !os.IsNotExist(err) {
log.Trace("Failed to remove file", "error", err, "file", tempFile)
}
```

#### Log File Cleanup
When cleaning up log files, use stderr instead of the logger to avoid recursion:

```go
func cleanupLogFile() {
if logFileHandle != nil {
if err := logFileHandle.Sync(); err != nil {
// Don't use logger here as we're cleaning up the log file
fmt.Fprintf(os.Stderr, "Warning: failed to sync log file: %v\n", err)
}
}
}
```

### Why This Matters

1. **Debugging**: When investigating issues, trace logs reveal the full story, including non-critical failures
2. **Metrics**: Aggregate trace logs to identify patterns (e.g., frequent cleanup failures might indicate disk issues)
3. **Auditing**: Complete error trail for compliance and security reviews
4. **Transparency**: No hidden errors - everything is recorded somewhere

### Common Squelched Error Categories

| Category | Examples | Trace Logging Required |
|----------|----------|----------------------|
| File cleanup | `os.Remove()`, `os.RemoveAll()` | ✅ Yes |
| Resource closing | `file.Close()`, `client.Close()` | ✅ Yes |
| Lock operations | `lock.Unlock()` | ✅ Yes |
| Config binding | `viper.BindEnv()`, `viper.BindPFlag()` | ✅ Yes |
| UI rendering | `fmt.Fprint()` to UI buffers | ✅ Yes |
| Command help | `cmd.Help()` | ✅ Yes |

---

## Common Anti-Patterns

### Using Logging as UI
Expand Down
Loading
Loading