Skip to content

Commit fde4d32

Browse files
committed
feat: add optional output to log file
use an env variable or a cli flag to enable it
1 parent 3149bf8 commit fde4d32

File tree

3 files changed

+95
-32
lines changed

3 files changed

+95
-32
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ If mandatory arguments are not provided, the program will prompt for them.
6565
| `--destination-big` | `DESTINATION_GITLAB_BIG` | No | Specify if the destination GitLab instance is a big instance (default: false) |
6666
| `--mirror-mapping` | `MIRROR_MAPPING` | Yes | Path to a JSON file containing the mirror mapping |
6767
| `--retry` or `-r` | N/A | No | Number of retries for failed GitLab API requests (default: 3) |
68+
| `--log-file` | `GITLAB_SYNC_LOG_FILE` | No | Path to a log file for output logs (default: `none`, only outputs logs to stderr) |
6869

6970
## Example
7071

cmd/main.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func main() {
2323
var args utils.ParserArgs
2424
var mirrorMappingPath string
2525
var timeout int
26+
var logFile string
2627

2728
var rootCmd = &cobra.Command{
2829
Use: "gitlab-sync",
@@ -31,7 +32,7 @@ func main() {
3132
Long: "Fully customizable gitlab repositories and groups mirroring between two (or one) gitlab instances.",
3233
Run: func(cmd *cobra.Command, cmdArgs []string) {
3334
// Set up the logger
34-
setupZapLogger(args.Verbose)
35+
setupZapLogger(args.Verbose, strings.TrimSpace(logFile))
3536
zap.L().Debug("Verbose mode enabled")
3637
zap.L().Debug("Parsing command line arguments")
3738

@@ -90,6 +91,7 @@ func main() {
9091
rootCmd.Flags().BoolVar(&args.DryRun, "dry-run", false, "Perform a dry run without making any changes")
9192
rootCmd.Flags().IntVarP(&timeout, "timeout", "t", 30, "Timeout in seconds for GitLab API requests")
9293
rootCmd.Flags().IntVarP(&args.Retry, "retry", "r", 3, "Number of retries for failed requests")
94+
rootCmd.Flags().StringVar(&logFile, "log-file", strings.TrimSpace(os.Getenv("GITLAB_SYNC_LOG_FILE")), "Path to the log file")
9395

9496
if err := rootCmd.Execute(); err != nil {
9597
zap.L().Error(err.Error())
@@ -133,10 +135,11 @@ func promptForMandatoryInput(defaultValue string, prompt string, errorMsg string
133135
return input
134136
}
135137

136-
// setupZapLogger sets up the Zap logger with the specified verbosity level.
138+
// setupZapLogger sets up the Zap logger with the specified verbosity level and optional file output.
137139
// It configures the logger to use ISO8601 time format and capitalizes the log levels.
138140
// The logger is set to production mode by default, but can be configured for debug mode if verbose is true.
139-
func setupZapLogger(verbose bool) {
141+
// If filename is not empty, the logger's output is redirected to the specified file.
142+
func setupZapLogger(verbose bool, filename string) {
140143
// Set up the logger configuration
141144
config := zap.NewProductionConfig()
142145
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
@@ -147,6 +150,15 @@ func setupZapLogger(verbose bool) {
147150
config.Level.SetLevel(zapcore.InfoLevel)
148151
}
149152

153+
// If a filename is specified, update the output path.
154+
if filename != "" {
155+
err := os.MkdirAll(filepath.Dir(filename), 0700)
156+
if err != nil {
157+
zap.L().Fatal("Failed to create log directory: " + err.Error())
158+
}
159+
config.OutputPaths = []string{filename, "stderr"}
160+
}
161+
150162
// Create the logger
151163
logger, err := config.Build()
152164
if err != nil {

cmd/main_test.go

Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,87 @@ const (
1414
RECEIVED_INPUT = "Received input"
1515
)
1616

17-
// setupZapLogger is assumed to be defined in the same module.
18-
// If it is in a different package, import that package accordingly.
19-
20-
func TestSetupZapLoggerVerbose(t *testing.T) {
21-
// Set up the logger in verbose mode.
22-
setupZapLogger(true)
23-
24-
// The global logger (zap.L()) should allow debug logs.
25-
// Check that debug logs are enabled.
26-
if !zap.L().Core().Enabled(zap.DebugLevel) {
27-
t.Error("Expected debug level to be enabled in verbose mode but it is not")
28-
}
29-
30-
// Optionally, check that Info level logs are enabled.
31-
if !zap.L().Core().Enabled(zap.InfoLevel) {
32-
t.Error("Expected info level to be enabled in verbose mode but it is not")
33-
}
34-
}
35-
36-
func TestSetupZapLoggerNonVerbose(t *testing.T) {
37-
// Set up the logger in non-verbose (production) mode.
38-
setupZapLogger(false)
39-
40-
// The global logger (zap.L()) should not allow debug logs.
41-
if zap.L().Core().Enabled(zap.DebugLevel) {
42-
t.Error("Expected debug level to be disabled in non-verbose mode but it is enabled")
17+
func TestSetupZapLogger(t *testing.T) {
18+
// Define the test cases in a table-driven approach.
19+
tests := []struct {
20+
name string
21+
verbose bool
22+
filename string
23+
debugExpected bool
24+
infoExpected bool
25+
shouldCreateFile bool
26+
}{
27+
{
28+
name: "Verbose without file",
29+
verbose: true,
30+
filename: "",
31+
debugExpected: true,
32+
infoExpected: true,
33+
shouldCreateFile: false,
34+
},
35+
{
36+
name: "Verbose with file",
37+
verbose: true,
38+
filename: "test_verbose.log",
39+
debugExpected: true,
40+
infoExpected: true,
41+
shouldCreateFile: true,
42+
},
43+
{
44+
name: "Non-Verbose without file",
45+
verbose: false,
46+
filename: "",
47+
debugExpected: false,
48+
infoExpected: true,
49+
shouldCreateFile: false,
50+
},
51+
{
52+
name: "Non-Verbose with file",
53+
verbose: false,
54+
filename: "test_nonverbose.log",
55+
debugExpected: false,
56+
infoExpected: true,
57+
shouldCreateFile: true,
58+
},
4359
}
4460

45-
// Info level logs must remain enabled.
46-
if !zap.L().Core().Enabled(zap.InfoLevel) {
47-
t.Error("Expected info level to be enabled in non-verbose mode but it is not")
61+
for _, tc := range tests {
62+
t.Run(tc.name, func(t *testing.T) {
63+
// Call the setup function with appropriate parameters.
64+
setupZapLogger(tc.verbose, tc.filename)
65+
66+
// Check that the logger's log level for debug is set as expected.
67+
if zap.L().Core().Enabled(zap.DebugLevel) != tc.debugExpected {
68+
t.Errorf("For verbose=%v, expected DebugLevel enabled to be %v, got %v",
69+
tc.verbose, tc.debugExpected, zap.L().Core().Enabled(zap.DebugLevel))
70+
}
71+
72+
// Check that info level is always enabled.
73+
if zap.L().Core().Enabled(zap.InfoLevel) != tc.infoExpected {
74+
t.Errorf("Expected InfoLevel enabled to be %v, got %v",
75+
tc.infoExpected, zap.L().Core().Enabled(zap.InfoLevel))
76+
}
77+
78+
// If a filename is provided, verify that the file is created.
79+
if tc.shouldCreateFile && tc.filename != "" {
80+
// Write a log entry to ensure that the logger flushes its output.
81+
zap.L().Info("test log entry")
82+
// Flush any buffered log entries.
83+
if err := zap.L().Sync(); err != nil {
84+
// On Windows, Sync may return an error, so we don't fail the test solely on that.
85+
t.Logf("Sync error (possibly expected on Windows): %v", err)
86+
}
87+
88+
// Check if the file exists.
89+
if _, err := os.Stat(tc.filename); os.IsNotExist(err) {
90+
t.Errorf("Expected log file %s to be created, but it does not exist", tc.filename)
91+
} else if err != nil {
92+
t.Errorf("Error checking log file %s: %v", tc.filename, err)
93+
}
94+
// Cleanup: remove the test log file.
95+
os.Remove(tc.filename)
96+
}
97+
})
4898
}
4999
}
50100

0 commit comments

Comments
 (0)