From 32af42919579fef40a724bf5fe6bb2a53455003e Mon Sep 17 00:00:00 2001 From: Yi Yang Date: Fri, 13 Dec 2024 09:53:53 +0800 Subject: [PATCH] fix: dont affect original go build command (#224) * fix: dont affect original go build command * update readme and usage --- README.md | 1 + docs/usage.md | 7 ++++ test/flags_test.go | 2 +- test/infra.go | 23 ++++------- tool/cmd/main.go | 44 +++++++++----------- tool/config/config.go | 64 +++++++++++++++-------------- tool/instrument/inst_file.go | 3 +- tool/instrument/inst_func.go | 3 +- tool/instrument/inst_struct.go | 3 +- tool/instrument/instrument.go | 11 +++-- tool/instrument/optimize.go | 7 ++-- tool/preprocess/fetch.go | 11 +++-- tool/preprocess/match.go | 29 ++++++------- tool/preprocess/pkgdep.go | 6 --- tool/preprocess/preprocess.go | 75 ++++++++++++++++++---------------- tool/preprocess/setup.go | 10 +---- tool/resource/bundle.go | 10 ++--- tool/shared/ast.go | 7 ++-- tool/shared/shared.go | 57 +++----------------------- tool/util/log.go | 45 ++++++++++++++++++++ tool/util/util.go | 72 ++++++++++++++++++++++++++++---- 21 files changed, 265 insertions(+), 225 deletions(-) create mode 100644 tool/util/log.go diff --git a/README.md b/README.md index b210978a..f00a8cb4 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ The configuration for the tool can be set by the following command: ```bash $ otel set -verbose # print verbose logs +$ otel set -log=/path/to/file.log # set log file $ otel set -debug # enable debug mode $ otel set -debug -verbose -rule=custom.json # set multiple configs $ otel set -disabledefault -rule=custom.json # disable default rules, use custom rules only diff --git a/docs/usage.md b/docs/usage.md index acb9225c..986d3a77 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -6,6 +6,13 @@ This guide provides a detailed overview of configuring and using the otel tool e ## Configuration The primary method of configuring the tool is through the `otel set` command. This command allows you to specify various settings tailored to your needs: +Logging: Set a custom log file to store logs generated by the tool. +```bash + $ otel set -log=/path/to/file.log +``` +The default log file is `.otel-build/preprocess/debug.log`. You can +log to stdout by setting `-log=/dev/stdout`. + Verbose Logging: Enable verbose logging to receive detailed output from the tool, which is helpful for troubleshooting and understanding the tool's processes. ```bash $ otel set -verbose diff --git a/test/flags_test.go b/test/flags_test.go index 5c363ee6..b0f51317 100644 --- a/test/flags_test.go +++ b/test/flags_test.go @@ -44,7 +44,7 @@ func TestFlags(t *testing.T) { RunGoBuild(t, "") } -func TestFlagConfigOverwriteNo(t *testing.T) { +func TestFlagEnvOverwrite(t *testing.T) { UseApp(AppName) RunSet(t, "-verbose=false") diff --git a/test/infra.go b/test/infra.go index 5def004c..6ffd01e7 100644 --- a/test/infra.go +++ b/test/infra.go @@ -55,7 +55,7 @@ func runCmd(args []string) *exec.Cmd { } func ReadInstrumentLog(t *testing.T, fileName string) string { - path := filepath.Join(shared.TempBuildDir, shared.PInstrument, fileName) + path := filepath.Join(shared.TempBuildDir, util.PInstrument, fileName) content, err := util.ReadFile(path) if err != nil { t.Fatal(err) @@ -64,7 +64,7 @@ func ReadInstrumentLog(t *testing.T, fileName string) string { } func ReadPreprocessLog(t *testing.T, fileName string) string { - path := filepath.Join(shared.TempBuildDir, shared.PPreprocess, fileName) + path := filepath.Join(shared.TempBuildDir, util.PPreprocess, fileName) content, err := util.ReadFile(path) if err != nil { t.Fatal(err) @@ -102,7 +102,6 @@ func RunSet(t *testing.T, args ...string) { func RunGoBuild(t *testing.T, args ...string) { util.Assert(pwd != "", "pwd is empty") - RunSet(t, "-debuglog") path := filepath.Join(filepath.Dir(pwd), getExecName()) cmd := runCmd(append([]string{path}, args...)) err := cmd.Run() @@ -113,17 +112,14 @@ func RunGoBuild(t *testing.T, args ...string) { t.Log("\n\n\n") t.Log(stderr) log1 := ReadPreprocessLog(t, shared.DebugLogFile) - log2 := ReadInstrumentLog(t, shared.DebugLogFile) text := fmt.Sprintf("failed to run instrument: %v\n", err) - text += fmt.Sprintf("preprocess: %v\n", log1) - text += fmt.Sprintf("instrument: %v\n", log2) + text += fmt.Sprintf("text: %v\n", log1) t.Fatal(text) } } func RunGoBuildWithEnv(t *testing.T, envs []string, args ...string) { util.Assert(pwd != "", "pwd is empty") - RunSet(t, "-debuglog") path := filepath.Join(filepath.Dir(pwd), getExecName()) cmd := runCmd(append([]string{path}, args...)) cmd.Env = append(cmd.Env, envs...) @@ -135,17 +131,14 @@ func RunGoBuildWithEnv(t *testing.T, envs []string, args ...string) { t.Log("\n\n\n") t.Log(stderr) log1 := ReadPreprocessLog(t, shared.DebugLogFile) - log2 := ReadInstrumentLog(t, shared.DebugLogFile) text := fmt.Sprintf("failed to run instrument: %v\n", err) - text += fmt.Sprintf("preprocess: %v\n", log1) - text += fmt.Sprintf("instrument: %v\n", log2) + text += fmt.Sprintf("text: %v\n", log1) t.Fatal(text) } } func RunGoBuildFallible(t *testing.T, args ...string) { util.Assert(pwd != "", "pwd is empty") - RunSet(t, "-debuglog") path := filepath.Join(filepath.Dir(pwd), getExecName()) cmd := runCmd(append([]string{path}, args...)) err := cmd.Run() @@ -219,25 +212,25 @@ func ExpectStderrContains(t *testing.T, expect string) { } func ExpectInstrumentContains(t *testing.T, log string, rule string) { - path := filepath.Join(shared.TempBuildDir, shared.PInstrument, log) + path := filepath.Join(shared.TempBuildDir, util.PInstrument, log) content := readLog(t, path) ExpectContains(t, content, rule) } func ExpectInstrumentNotContains(t *testing.T, log string, rule string) { - path := filepath.Join(shared.TempBuildDir, shared.PInstrument, log) + path := filepath.Join(shared.TempBuildDir, util.PInstrument, log) content := readLog(t, path) ExpectNotContains(t, content, rule) } func ExpectPreprocessContains(t *testing.T, log string, rule string) { - path := filepath.Join(shared.TempBuildDir, shared.PPreprocess, log) + path := filepath.Join(shared.TempBuildDir, util.PPreprocess, log) content := readLog(t, path) ExpectContains(t, content, rule) } func ExpectPreprocessNotContains(t *testing.T, log string, rule string) { - path := filepath.Join(shared.TempBuildDir, shared.PPreprocess, log) + path := filepath.Join(shared.TempBuildDir, util.PPreprocess, log) content := readLog(t, path) ExpectNotContains(t, content, rule) } diff --git a/tool/cmd/main.go b/tool/cmd/main.go index cc35f5dd..703638db 100644 --- a/tool/cmd/main.go +++ b/tool/cmd/main.go @@ -16,7 +16,6 @@ package main import ( "fmt" - "log" "os" "path/filepath" "strings" @@ -49,18 +48,17 @@ Command: ` func printUsage() { - usage = strings.ReplaceAll(usage, "{}", config.GetToolName()) + usage = strings.ReplaceAll(usage, "{}", util.GetToolName()) fmt.Print(usage) } -func initLogs(names ...string) error { - for _, name := range names { - path := shared.GetTempBuildDirWith(name) - logPath := filepath.Join(path, shared.DebugLogFile) - _, err := os.Create(logPath) - if err != nil { - return fmt.Errorf("failed to create log file: %w", err) - } +func initLog() error { + name := util.PPreprocess + path := shared.GetTempBuildDirWith(name) + logPath := filepath.Join(path, shared.DebugLogFile) + _, err := os.Create(logPath) + if err != nil { + return fmt.Errorf("failed to create log file: %w", err) } return nil } @@ -68,7 +66,7 @@ func initLogs(names ...string) error { func initTempDir() error { // All temp directories are prepared before, instrument phase should not // create any new directories. - if shared.GetRunPhase() == shared.PInstrument { + if util.GetRunPhase() == util.PInstrument { return nil } @@ -83,14 +81,14 @@ func initTempDir() error { // we always recreate the preprocess and instrument directories, but only // create the configure directory if it does not exist. This is because // the configure directory can be used across multiple runs. - exist, _ := util.PathExists(shared.GetTempBuildDirWith(shared.PConfigure)) + exist, _ := util.PathExists(shared.GetTempBuildDirWith(util.PConfigure)) if !exist { - err := os.MkdirAll(shared.GetTempBuildDirWith(shared.PConfigure), 0777) + err := os.MkdirAll(shared.GetTempBuildDirWith(util.PConfigure), 0777) if err != nil { return fmt.Errorf("failed to make log directory: %w", err) } } - for _, subdir := range []string{shared.PPreprocess, shared.PInstrument} { + for _, subdir := range []string{util.PPreprocess, util.PInstrument} { exist, _ = util.PathExists(shared.GetTempBuildDirWith(subdir)) if exist { err := os.RemoveAll(shared.GetTempBuildDirWith(subdir)) @@ -114,19 +112,17 @@ func initEnv() error { switch { case os.Args[1] == SubcommandSet: // otel set? - shared.SetRunPhase(shared.PConfigure) + util.SetRunPhase(util.PConfigure) case strings.HasSuffix(os.Args[1], SubcommandGo): // otel go build? - shared.SetRunPhase(shared.PPreprocess) + util.SetRunPhase(util.PPreprocess) case os.Args[1] == SubcommandRemix: // otel remix? - shared.SetRunPhase(shared.PInstrument) + util.SetRunPhase(util.PInstrument) default: // do nothing } - log.SetPrefix("[" + shared.GetRunPhase().String() + "] ") - // Create temp build directory err := initTempDir() if err != nil { @@ -134,15 +130,15 @@ func initEnv() error { } // Create log files under temp build directory - if shared.InPreprocess() { - err := initLogs(shared.PPreprocess, shared.PInstrument) + if util.InPreprocess() { + err := initLog() if err != nil { return fmt.Errorf("failed to init logs: %w", err) } } // Prepare shared configuration - if shared.InPreprocess() || shared.InInstrument() { + if util.InPreprocess() || util.InInstrument() { err = config.InitConfig() if err != nil { return fmt.Errorf("failed to init config: %w", err) @@ -159,7 +155,7 @@ func main() { err := initEnv() if err != nil { - log.Printf("failed to init env: %v", err) + util.LogFatal("failed to init env: %v", err) os.Exit(1) } @@ -177,7 +173,7 @@ func main() { printUsage() } if err != nil { - log.Printf("failed to run command %s: %v", subcmd, err) + util.LogFatal("failed to run command %s: %v", subcmd, err) os.Exit(1) } } diff --git a/tool/config/config.go b/tool/config/config.go index ee9f42a7..f68c0d94 100644 --- a/tool/config/config.go +++ b/tool/config/config.go @@ -18,7 +18,6 @@ import ( "encoding/json" "flag" "fmt" - "log" "os" "path/filepath" "reflect" @@ -42,8 +41,8 @@ type BuildConfig struct { // default rules, you can configure -disabledefault flag in advance. RuleJsonFiles string - // DebugLog true means debug log is enabled. - DebugLog bool + // Log specifies the log file path. If not set, log will be saved to file. + Log string // Verbose true means print verbose log. Verbose bool @@ -62,20 +61,10 @@ type BuildConfig struct { // is passed. This value is specified by the build system. var ToolVersion = "1.0.0" -var GetToolName = func() string { - // Get the path of the current executable - ex, err := os.Executable() - if err != nil { - log.Fatalf("failed to get executable: %v", err) - os.Exit(0) - } - return filepath.Base(ex) -} - var conf *BuildConfig func GetConf() *BuildConfig { - util.Assert(!shared.InConfigure(), "called in configure") + util.Assert(!util.InConfigure(), "called in configure") util.Assert(conf != nil, "build config is not initialized") return conf } @@ -100,7 +89,7 @@ func (bc *BuildConfig) makeRuleAbs(file string) (string, error) { } func (bc *BuildConfig) parseRuleFiles() error { - if shared.InInstrument() { + if util.InInstrument() { return nil } // Get absolute path of rule file, otherwise instrument will not @@ -131,7 +120,7 @@ func (bc *BuildConfig) parseRuleFiles() error { func storeConfig(bc *BuildConfig) error { util.Assert(bc != nil, "build config is not initialized") - util.Assert(shared.InConfigure(), "sanity check") + util.Assert(util.InConfigure(), "sanity check") file := shared.GetConfigureLogPath(shared.BuildConfFile) bs, err := json.Marshal(bc) @@ -188,15 +177,17 @@ func loadConfigFromEnv(conf *BuildConfig) { // Environment variables are able to overwrite the config items even if the // config file sets them. The environment variable name is the upper snake // case of the config item name, prefixed with "OTELTOOL_". For example, the - // environment variable for "DebugLog" is "OTELTOOL_DEBUG_LOG". + // environment variable for "Log" is "OTELTOOL_LOG". typ := reflect.TypeOf(*conf) for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) envKey := fmt.Sprintf("%s%s", EnvPrefix, toUpperSnakeCase(field.Name)) envVal := os.Getenv(envKey) if envVal != "" { - log.Printf("Overwrite config %s with environment variable %s", - field.Name, envKey) + if util.InPreprocess() { + util.Log("Overwrite config %s with environment variable %s", + field.Name, envKey) + } v := reflect.ValueOf(conf).Elem() f := v.FieldByName(field.Name) switch f.Kind() { @@ -205,7 +196,7 @@ func loadConfigFromEnv(conf *BuildConfig) { case reflect.String: f.SetString(envVal) default: - log.Fatalf("Unsupported config type %s", f.Kind()) + util.LogFatal("Unsupported config type %s", f.Kind()) } } } @@ -224,33 +215,46 @@ func InitConfig() (err error) { return fmt.Errorf("failed to parse rule files: %w", err) } - if conf.DebugLog { - // Redirect log to debug log if required - debugLogPath := shared.GetLogPath(shared.DebugLogFile) - debugLog, _ := os.OpenFile(debugLogPath, os.O_WRONLY|os.O_APPEND, 0777) + mode := os.O_WRONLY | os.O_APPEND + if util.InPreprocess() { + // We always create log file in preprocess phase, but in further + // instrument phase, we append log content to the existing file. + mode = os.O_WRONLY | os.O_CREATE | os.O_TRUNC + } + if conf.Log == "" { + // Redirect log to file if flag is not set + debugLogPath := shared.GetPreprocessLogPath(shared.DebugLogFile) + debugLog, _ := os.OpenFile(debugLogPath, mode, 0777) if debugLog != nil { - log.SetOutput(debugLog) + util.SetLogTo(debugLog) + } + } else { + // Otherwise, log to the specified file + logFile, err := os.OpenFile(conf.Log, mode, 0777) + if err != nil { + return fmt.Errorf("failed to open log file: %w", err) } + util.SetLogTo(logFile) } return nil } func PrintVersion() error { - fmt.Printf("%s version %s\n", GetToolName(), ToolVersion) + fmt.Printf("%s version %s\n", util.GetToolName(), ToolVersion) os.Exit(0) return nil } func Configure() error { - shared.GuaranteeInConfigure() + util.GuaranteeInConfigure() // Parse command line flags to get build config bc, err := loadConfig() if err != nil { bc = &BuildConfig{} } - flag.BoolVar(&bc.DebugLog, "debuglog", bc.DebugLog, - "Print debug log to file") + flag.StringVar(&bc.Log, "log", bc.Log, + "Log file path. If not set, log will be saved to file.") flag.BoolVar(&bc.Verbose, "verbose", bc.Verbose, "Print verbose log") flag.BoolVar(&bc.Debug, "debug", bc.Debug, @@ -263,7 +267,7 @@ func Configure() error { "Disable default rules") flag.CommandLine.Parse(os.Args[2:]) - fmt.Printf("Configured in %s", + util.Log("Configured in %s", shared.GetConfigureLogPath(shared.BuildConfFile)) // Store build config for future phases diff --git a/tool/instrument/inst_file.go b/tool/instrument/inst_file.go index c1d9cc83..10299002 100644 --- a/tool/instrument/inst_file.go +++ b/tool/instrument/inst_file.go @@ -16,7 +16,6 @@ package instrument import ( "fmt" - "log" "path/filepath" "strings" @@ -62,7 +61,7 @@ func (rp *RuleProcessor) applyFileRules(bundle *resource.RuleBundle) (err error) } else { rp.addCompileArg(target) } - log.Printf("Apply file rule %v", rule) + util.Log("Apply file rule %v", rule) rp.saveDebugFile(target) } return nil diff --git a/tool/instrument/inst_func.go b/tool/instrument/inst_func.go index 7ac52833..6d2c64a6 100644 --- a/tool/instrument/inst_func.go +++ b/tool/instrument/inst_func.go @@ -16,7 +16,6 @@ package instrument import ( "fmt" - "log" "os" "path/filepath" "regexp" @@ -371,7 +370,7 @@ func (rp *RuleProcessor) applyFuncRules(bundle *resource.RuleBundle) (err error) return fmt.Errorf("failed to rewrite: %w for %v", err, rule) } - log.Printf("Apply func rule %s\n", rule) + util.Log("Apply func rule %s", rule) } break } diff --git a/tool/instrument/inst_struct.go b/tool/instrument/inst_struct.go index d4242042..aaa5057d 100644 --- a/tool/instrument/inst_struct.go +++ b/tool/instrument/inst_struct.go @@ -16,7 +16,6 @@ package instrument import ( "fmt" - "log" "github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/resource" "github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/shared" @@ -27,7 +26,7 @@ import ( func addStructField(rule *resource.InstStructRule, decl dst.Decl) { util.Assert(rule.FieldName != "" && rule.FieldType != "", "rule must have field and type") - log.Printf("Apply struct rule %v", rule) + util.Log("Apply struct rule %v", rule) shared.AddStructField(decl, rule.FieldName, rule.FieldType) } diff --git a/tool/instrument/instrument.go b/tool/instrument/instrument.go index 097ac765..c80ddab5 100644 --- a/tool/instrument/instrument.go +++ b/tool/instrument/instrument.go @@ -17,7 +17,6 @@ package instrument import ( "errors" "fmt" - "log" "os" "path/filepath" "strings" @@ -128,12 +127,12 @@ func (rp *RuleProcessor) saveDebugFile(path string) { dest = shared.GetInstrumentLogPath(dest) err := os.MkdirAll(filepath.Dir(dest), os.ModePerm) if err != nil { // error is tolerable here - log.Printf("failed to create debug file directory %s: %v", dest, err) + util.Log("failed to create debug file directory %s: %v", dest, err) return } err = util.CopyFile(path, dest) if err != nil { // error is tolerable here - log.Printf("failed to save debug file %s: %v", dest, err) + util.Log("failed to save debug file %s: %v", dest, err) } } @@ -216,7 +215,7 @@ func compileRemix(bundle *resource.RuleBundle, args []string) error { } // Good, run final compilation after instrumentation err = util.RunCmd(rp.compileArgs...) - log.Printf("RunCmd: %v (%v)\n", + util.Log("RunCmd: %v (%v)", bundle.ImportPath, rp.compileArgs) return err } @@ -227,7 +226,7 @@ func Instrument() error { // Is compile command? if shared.IsCompileCommand(strings.Join(args, " ")) { if config.GetConf().Verbose { - log.Printf("RunCmd: %v\n", args) + util.Log("RunCmd: %v", args) } bundles, err := resource.LoadRuleBundles() if err != nil { @@ -237,7 +236,7 @@ func Instrument() error { util.Assert(bundle.IsValid(), "sanity check") // Is compiling the target package? if matchImportPath(bundle.ImportPath, args) { - log.Printf("Apply bundle %v\n", bundle) + util.Log("Apply bundle %v", bundle) return compileRemix(bundle, args) } } diff --git a/tool/instrument/optimize.go b/tool/instrument/optimize.go index 3af1d3aa..d157e0c8 100644 --- a/tool/instrument/optimize.go +++ b/tool/instrument/optimize.go @@ -16,7 +16,6 @@ package instrument import ( "fmt" - "log" "strings" "github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/config" @@ -122,7 +121,7 @@ func (rp *RuleProcessor) removeOnExitTrampolineCall(tjump *TJump) error { // trampoline-jump-if inlining work elseBlock.List[i] = newDecoratedEmptyStmt() if config.GetConf().Verbose { - log.Printf("Optimize tjump branch in %s", + util.Log("Optimize tjump branch in %s", tjump.target.Name.Name) } break @@ -186,7 +185,7 @@ func (rp *RuleProcessor) removeOnEnterTrampolineCall(tjump *TJump) error { tjump.ifStmt.Cond = shared.BoolFalse() tjump.ifStmt.Body = shared.Block(newDecoratedEmptyStmt()) if config.GetConf().Verbose { - log.Printf("Optimize tjump branch in %s", tjump.target.Name.Name) + util.Log("Optimize tjump branch in %s", tjump.target.Name.Name) } // Remove generated onEnter trampoline function removed := rp.removeDeclWhen(func(d dst.Decl) bool { @@ -222,7 +221,7 @@ func flattenTJump(tjump *TJump, removedOnExit bool) { shared.MakeUnusedIdent(skipCallIdent) } if config.GetConf().Verbose { - log.Printf("Optimize skipCall in %s", tjump.target.Name.Name) + util.Log("Optimize skipCall in %s", tjump.target.Name.Name) } } diff --git a/tool/preprocess/fetch.go b/tool/preprocess/fetch.go index 2f8a0404..019c142b 100644 --- a/tool/preprocess/fetch.go +++ b/tool/preprocess/fetch.go @@ -18,7 +18,6 @@ import ( "encoding/json" "fmt" "io/fs" - "log" "os" "path/filepath" "strings" @@ -100,7 +99,7 @@ func (dp *DepProcessor) fetchEmbed(path string) (string, error) { return fmt.Errorf("failed to write file: %w", err) } if config.GetConf().Verbose { - log.Printf("Copy embed file %v to %v", p, target) + util.Log("Copy embed file %v to %v", p, target) } return nil } @@ -122,7 +121,7 @@ func (dp *DepProcessor) fetchFrom(path string) (string, error) { return "", fmt.Errorf("failed to check path: %w", err) } if exist { - log.Printf("Fetch %s from local file system", path) + util.Log("Fetch %s from local file system", path) return path, nil } // Path to network @@ -135,7 +134,7 @@ func (dp *DepProcessor) fetchFrom(path string) (string, error) { if err != nil { return "", fmt.Errorf("failed to fetch embed: %w", err) } - log.Printf("Fetch %s from embed cache", path) + util.Log("Fetch %s from embed cache", path) return dir, nil } @@ -145,7 +144,7 @@ func (dp *DepProcessor) fetchFrom(path string) (string, error) { return "", fmt.Errorf("failed to download module: %w", err) } // Get path to the local module cache - log.Printf("Fetch %s from network %s", path, dir) + util.Log("Fetch %s from network %s", path, dir) return dir, nil } @@ -155,7 +154,7 @@ func (dp *DepProcessor) fetchFrom(path string) (string, error) { // fetchRules fetches the rules via the network func (dp *DepProcessor) fetchRules() error { - shared.GuaranteeInPreprocess() + util.GuaranteeInPreprocess() defer util.PhaseTimer("Fetch")() // Different rules may share the same path, we dont want to fetch the same // path multiple times, so we use a map to record the resolved paths diff --git a/tool/preprocess/match.go b/tool/preprocess/match.go index 99cd3866..11db432c 100644 --- a/tool/preprocess/match.go +++ b/tool/preprocess/match.go @@ -17,7 +17,6 @@ package preprocess import ( "encoding/json" "fmt" - "log" "os" "strings" @@ -39,7 +38,7 @@ func newRuleMatcher() *ruleMatcher { rules[rule.GetImportPath()] = append(rules[rule.GetImportPath()], rule) } if config.GetConf().Verbose { - log.Printf("Available rules: %v", rules) + util.Log("Available rules: %v", rules) } return &ruleMatcher{availableRules: rules} } @@ -91,14 +90,14 @@ func loadRuleRaw(content string) ([]resource.InstRule, error) { func loadDefaultRules() []resource.InstRule { rules, err := loadRuleRaw(pkg.ExportDefaultRuleJson()) if err != nil { - log.Printf("Failed to load default rules: %v", err) + util.Log("Failed to load default rules: %v", err) return nil } return rules } func findAvailableRules() []resource.InstRule { - shared.GuaranteeInPreprocess() + util.GuaranteeInPreprocess() // Disable all instrumentation rules and rebuild the whole project to restore // all instrumentation actions, this also reverts the modification on Golang // runtime package. @@ -122,7 +121,7 @@ func findAvailableRules() []resource.InstRule { for _, ruleFile := range ruleFiles { r, err := loadRuleFile(ruleFile) if err != nil { - log.Printf("Failed to load rules: %v", err) + util.Log("Failed to load rules: %v", err) continue } rules = append(rules, r...) @@ -132,7 +131,7 @@ func findAvailableRules() []resource.InstRule { // Load the one rule file rs, err := loadRuleFile(config.GetConf().RuleJsonFiles) if err != nil { - log.Printf("Failed to load rules: %v", err) + util.Log("Failed to load rules: %v", err) return nil } rules = append(rules, rs...) @@ -170,7 +169,7 @@ func (rm *ruleMatcher) match(importPath string, // Check if the version is supported matched, err := shared.MatchVersion(version, rule.GetVersion()) if err != nil { - log.Printf("Failed to match version %v between %v and %v", + util.Log("Failed to match version %v between %v and %v", err, file, rule) continue } @@ -182,10 +181,10 @@ func (rm *ruleMatcher) match(importPath string, if _, ok := rule.(*resource.InstFileRule); ok { ast, err := shared.ParseAstFromFileOnlyPackage(file) if ast == nil || err != nil { - log.Printf("Failed to parse %s: %v", file, err) + util.Log("Failed to parse %s: %v", file, err) continue } - log.Printf("Match file rule %s", rule) + util.Log("Match file rule %s", rule) bundle.AddFileRule(rule.(*resource.InstFileRule)) bundle.SetPackageName(ast.Name.Name) availables = append(availables[:i], availables[i+1:]...) @@ -197,7 +196,7 @@ func (rm *ruleMatcher) match(importPath string, if _, ok := parsedAst[file]; !ok { fileAst, err := shared.ParseAstFromFileFast(file) if fileAst == nil || err != nil { - log.Printf("failed to parse file %s: %v", file, err) + util.Log("failed to parse file %s: %v", file, err) continue } parsedAst[file] = fileAst @@ -211,7 +210,7 @@ func (rm *ruleMatcher) match(importPath string, if tree == nil { // Failed to parse the file, stop here and log only // sicne it's a tolerant failure - log.Printf("Failed to parse file %s", file) + util.Log("Failed to parse file %s", file) continue } @@ -221,7 +220,7 @@ func (rm *ruleMatcher) match(importPath string, if genDecl, ok := decl.(*dst.GenDecl); ok { if rl, ok := rule.(*resource.InstStructRule); ok { if shared.MatchStructDecl(genDecl, rl.StructType) { - log.Printf("Match struct rule %s", rule) + util.Log("Match struct rule %s", rule) bundle.AddFile2StructRule(file, rl) valid = true break @@ -231,7 +230,7 @@ func (rm *ruleMatcher) match(importPath string, if rl, ok := rule.(*resource.InstFuncRule); ok { if shared.MatchFuncDecl(funcDecl, rl.Function, rl.ReceiverType) { - log.Printf("Match func rule %s", rule) + util.Log("Match func rule %s", rule) bundle.AddFile2FuncRule(file, rl) valid = true break @@ -262,9 +261,7 @@ func runMatch(matcher *ruleMatcher, cmd string, ch chan *resource.RuleBundle) { cmdArgs := shared.SplitCmds(cmd) importPath := readImportPath(cmdArgs) util.Assert(importPath != "", "sanity check") - if config.GetConf().Verbose { - log.Printf("Matching %v with %v\n", importPath, cmdArgs) - } + util.Log("RunMatch: %v (%v)", importPath, cmdArgs) bundle := matcher.match(importPath, cmdArgs) ch <- bundle } diff --git a/tool/preprocess/pkgdep.go b/tool/preprocess/pkgdep.go index a4126853..22d45dd5 100644 --- a/tool/preprocess/pkgdep.go +++ b/tool/preprocess/pkgdep.go @@ -15,10 +15,8 @@ package preprocess import ( "fmt" - "log" "strings" - "github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/config" "github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/resource" "github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/shared" "github.com/alibaba/opentelemetry-go-auto-instrumentation/tool/util" @@ -55,10 +53,6 @@ func (dp *DepProcessor) replaceOtelImports() error { if err != nil { return fmt.Errorf("failed to read file content: %w", err) } - if config.GetConf().Verbose { - log.Printf("Replace import path of %s to %s", file, moduleName) - } - content = replaceImport(moduleName, content) _, err = util.WriteFile(file, content) if err != nil { diff --git a/tool/preprocess/preprocess.go b/tool/preprocess/preprocess.go index cdb3c0c2..2b358791 100644 --- a/tool/preprocess/preprocess.go +++ b/tool/preprocess/preprocess.go @@ -18,7 +18,6 @@ import ( "bufio" "embed" "fmt" - "log" "os" "os/exec" "os/signal" @@ -132,7 +131,7 @@ func newDepProcessor() *DepProcessor { s := <-sigc switch s { case syscall.SIGTERM, syscall.SIGINT: - log.Printf("Interrupted instrumentation, cleaning up") + util.Log("Interrupted instrumentation, cleaning up") dp.postProcess() default: } @@ -141,13 +140,7 @@ func newDepProcessor() *DepProcessor { } func (dp *DepProcessor) postProcess() { - shared.GuaranteeInPreprocess() - // Clean build cache as we may instrument some std packages(e.g. runtime) - // TODO: fine-grained cache cleanup - // err := util.RunCmd("go", "clean", "-cache") - // if err != nil { - // log.Fatalf("failed to clean cache: %v", err) - // } + util.GuaranteeInPreprocess() // Using -debug? Leave all changes for debugging if config.GetConf().Debug { @@ -163,12 +156,12 @@ func (dp *DepProcessor) postProcess() { // Restore everything we have modified during instrumentation err := dp.restoreBackupFiles() if err != nil { - log.Fatalf("failed to restore: %v", err) + util.LogFatal("failed to restore: %v", err) } } func (dp *DepProcessor) backupFile(origin string) error { - shared.GuaranteeInPreprocess() + util.GuaranteeInPreprocess() backup := filepath.Base(origin) + OtelBackupSuffix backup = shared.GetLogPath(filepath.Join(OtelBackups, backup)) err := os.MkdirAll(filepath.Dir(backup), 0777) @@ -181,21 +174,21 @@ func (dp *DepProcessor) backupFile(origin string) error { return fmt.Errorf("failed to backup file %v: %w", origin, err) } dp.backups[origin] = backup - log.Printf("Backup %v\n", origin) + util.Log("Backup %v", origin) } else if config.GetConf().Verbose { - log.Printf("Backup %v already exists\n", origin) + util.Log("Backup %v already exists", origin) } return nil } func (dp *DepProcessor) restoreBackupFiles() error { - shared.GuaranteeInPreprocess() + util.GuaranteeInPreprocess() for origin, backup := range dp.backups { err := util.CopyFile(backup, origin) if err != nil { return err } - log.Printf("Restore %v\n", origin) + util.Log("Restore %v", origin) } return nil } @@ -208,7 +201,7 @@ func getCompileCommands() ([]string, error) { defer func(dryRunLog *os.File) { err := dryRunLog.Close() if err != nil { - log.Printf("Failed to close dry run log file: %v", err) + util.Log("Failed to close dry run log file: %v", err) } }(dryRunLog) @@ -320,7 +313,7 @@ func (dp *DepProcessor) addExplicitImport(importPaths ...string) (err error) { for _, importPath := range importPaths { shared.AddImportForcely(astRoot, importPath) if config.GetConf().Verbose { - log.Printf("Add %s import to %v", importPath, file) + util.Log("Add %s import to %v", importPath, file) } } addImport = true @@ -374,9 +367,13 @@ func (dp *DepProcessor) findLocalImportPath() error { if err != nil { return fmt.Errorf("failed to get module name: %w", err) } + // Replace all backslashes with slashes. The import path is different from + // the file path, which should always use slashes. + workingDir = filepath.ToSlash(workingDir) + projectDir = filepath.ToSlash(projectDir) dp.localImportPath = strings.Replace(workingDir, projectDir, moduleName, 1) if config.GetConf().Verbose { - log.Printf("Find local import path: %v", dp.localImportPath) + util.Log("Find local import path: %v", dp.localImportPath) } return nil } @@ -421,13 +418,13 @@ func (dp *DepProcessor) preclean() { } if shared.RemoveImport(astRoot, ruleImport) != nil { if config.GetConf().Verbose { - log.Printf("Remove obsolete import %v from %v", + util.Log("Remove obsolete import %v from %v", ruleImport, file) } } _, err := shared.WriteAstToFile(astRoot, file) if err != nil { - log.Printf("Failed to write ast to %v: %v", file, err) + util.Log("Failed to write ast to %v: %v", file, err) } } // Clean otel_rules/otel_pkgdep directory @@ -469,19 +466,27 @@ func runDryBuild(goBuildCmd []string) error { } func runModTidy() error { - return util.RunCmd("go", "mod", "tidy") + out, err := util.RunCmdOutput("go", "mod", "tidy") + util.Log("Run go mod tidy: %v", out) + return err } func runGoGet(dep string) error { - return util.RunCmd("go", "get", dep) + out, err := util.RunCmdOutput("go", "get", dep) + util.Log("Run go get %v: %v", dep, out) + return err } func runGoModDownload(path string) error { - return util.RunCmd("go", "mod", "download", path) + out, err := util.RunCmdOutput("go", "mod", "download", path) + util.Log("Run go mod download %v: %v", path, out) + return err } func runGoModEdit(require string) error { - return util.RunCmd("go", "mod", "edit", "-require="+require) + out, err := util.RunCmdOutput("go", "mod", "edit", "-require="+require) + util.Log("Run go mod edit %v: %v", require, out) + return err } func runCleanCache() error { @@ -495,10 +500,10 @@ func nullDevice() string { return "/dev/null" } -func runBuildWithToolexec(goBuildCmd []string) (string, error) { +func runBuildWithToolexec(goBuildCmd []string) error { exe, err := os.Executable() if err != nil { - return "", err + return err } args := []string{ "go", @@ -528,11 +533,13 @@ func runBuildWithToolexec(goBuildCmd []string) (string, error) { } if config.GetConf().Verbose { - log.Printf("Run go build with args %v in toolexec mode", args) + util.Log("Run go build with args %v in toolexec mode", args) } args = util.StringDedup(args) shared.AssertGoBuild(args) - return util.RunCmdOutput(args...) + out, err := util.RunCmdOutput(args...) + util.Log("Run go build with toolexec: %v", out) + return err } func fetchDep(path string) error { @@ -560,12 +567,12 @@ func (dp *DepProcessor) pinDepVersion() error { p := dep.dep v := dep.version if config.GetConf().Verbose { - log.Printf("Pin dependency version %v@%v", p, v) + util.Log("Pin dependency version %v@%v", p, v) } err := fetchDep(p + "@" + v) if err != nil { if dep.fallible { - log.Printf("Failed to pin dependency %v: %v", p, err) + util.Log("Failed to pin dependency %v: %v", p, err) continue } return fmt.Errorf("failed to pin dependency %v: %w", dep, err) @@ -765,13 +772,11 @@ func Preprocess() error { defer util.PhaseTimer("Instrument")() // Run go build with toolexec to start instrumentation - out, err := runBuildWithToolexec(dp.goBuildCmd) + err = runBuildWithToolexec(dp.goBuildCmd) if err != nil { - return fmt.Errorf("failed to run go toolexec build: %w\n%s", - err, out) - } else { - log.Printf("CompileRemix: %s", out) + return fmt.Errorf("failed to run go toolexec build: %w", err) } } + util.Log("Build completed successfully") return nil } diff --git a/tool/preprocess/setup.go b/tool/preprocess/setup.go index a439b7b5..2c682437 100644 --- a/tool/preprocess/setup.go +++ b/tool/preprocess/setup.go @@ -17,7 +17,6 @@ package preprocess import ( "fmt" "go/token" - "log" "os" "path/filepath" "strings" @@ -54,8 +53,6 @@ func (dp *DepProcessor) copyRules(pkgName string) (err error) { return nil } // Find out which resource files we should add to project - // uniqueResources := make(map[string]*resource.RuleBundle) - // res2Dir := make(map[string]string) for _, bundle := range dp.bundles { for _, funcRules := range bundle.File2FuncRules { // Copy resource file into project as otel_rule_\d.go @@ -85,9 +82,6 @@ func (dp *DepProcessor) copyRules(pkgName string) (err error) { for _, file := range files { if !shared.IsGoFile(file) || shared.IsGoTestFile(file) { - if config.GetConf().Verbose { - log.Printf("Ignore file %v\n", file) - } continue } @@ -172,7 +166,7 @@ func makeHookPublic(astRoot *dst.File, bundle *resource.RuleBundle) { for _, param := range params { if sele, ok := param.Type.(*dst.SelectorExpr); ok { if _, ok := sele.X.(*dst.Ident); ok { - if sele.Sel.Name == "CallContext" { + if sele.Sel.Name == ApiCallContext { f.Name.Name = strings.Title(f.Name.Name) break } @@ -209,7 +203,7 @@ func (dp *DepProcessor) copyRule(path, target string, return fmt.Errorf("failed to write ast to %v: %w", target, err) } if config.GetConf().Verbose { - log.Printf("Copy dependency %v to %v", path, target) + util.Log("Copy dependency %v to %v", path, target) } return nil } diff --git a/tool/resource/bundle.go b/tool/resource/bundle.go index 450caaa4..a3f5de78 100644 --- a/tool/resource/bundle.go +++ b/tool/resource/bundle.go @@ -116,7 +116,7 @@ func FindHookFile(rule *InstFuncRule) (string, error) { } root, err := shared.ParseAstFromFileFast(file) if err != nil { - return "", fmt.Errorf("failed to read file: %w", err) + return "", fmt.Errorf("failed to read hook file: %w", err) } if isHookDefined(root, rule) { return file, nil @@ -140,11 +140,11 @@ func FindRuleFiles(rule InstRule) ([]string, error) { } func StoreRuleBundles(bundles []*RuleBundle) error { - shared.GuaranteeInPreprocess() + util.GuaranteeInPreprocess() ruleFile := shared.GetPreprocessLogPath(RuleBundleJsonFile) bs, err := json.Marshal(bundles) if err != nil { - return fmt.Errorf("failed to marshal bundles: %w", err) + return fmt.Errorf("failed to store used rules: %w", err) } _, err = util.WriteFile(ruleFile, string(bs)) if err != nil { @@ -154,7 +154,7 @@ func StoreRuleBundles(bundles []*RuleBundle) error { } func LoadRuleBundles() ([]*RuleBundle, error) { - shared.GuaranteeInInstrument() + util.GuaranteeInInstrument() ruleFile := shared.GetPreprocessLogPath(RuleBundleJsonFile) data, err := util.ReadFile(ruleFile) @@ -164,7 +164,7 @@ func LoadRuleBundles() ([]*RuleBundle, error) { var bundles []*RuleBundle err = json.Unmarshal([]byte(data), &bundles) if err != nil { - return nil, fmt.Errorf("failed to unmarshal bundles: %w", err) + return nil, fmt.Errorf("failed to load used rules: %w", err) } return bundles, nil } diff --git a/tool/shared/ast.go b/tool/shared/ast.go index 5a8d0e0f..95ac3f5e 100644 --- a/tool/shared/ast.go +++ b/tool/shared/ast.go @@ -18,7 +18,6 @@ import ( "fmt" "go/parser" "go/token" - "log" "os" "path/filepath" @@ -235,7 +234,7 @@ func SwitchCase(list []dst.Expr, stmts []dst.Stmt) *dst.CaseClause { func AddStructField(decl dst.Decl, name string, typ string) { gen, ok := decl.(*dst.GenDecl) if !ok { - log.Fatalf("decl is not a GenDecl") + util.LogFatal("decl is not a GenDecl") } fd := NewField(name, Ident(typ)) st := gen.Specs[0].(*dst.TypeSpec).Type.(*dst.StructType) @@ -427,7 +426,7 @@ func parseAstMode(filePath string, mode parser.Mode) (*dst.File, error) { defer func(file *os.File) { err := file.Close() if err != nil { - log.Fatalf("failed to close file %s: %v", file.Name(), err) + util.LogFatal("failed to close file %s: %v", file.Name(), err) } }(file) astFile, err := parser.ParseFile(fset, name, file, mode) @@ -464,7 +463,7 @@ func WriteAstToFile(astRoot *dst.File, filePath string) (string, error) { defer func(file *os.File) { err := file.Close() if err != nil { - log.Fatalf("failed to close file %s: %v", file.Name(), err) + util.LogFatal("failed to close file %s: %v", file.Name(), err) } }(file) diff --git a/tool/shared/shared.go b/tool/shared/shared.go index b42638c0..1772505d 100644 --- a/tool/shared/shared.go +++ b/tool/shared/shared.go @@ -42,29 +42,6 @@ const ( BuildConfFile = "build_conf.json" ) -type RunPhase string - -const ( - PInvalid = "invalid" - PPreprocess = "preprocess" - PInstrument = "instrument" - PConfigure = "configure" -) - -var rp RunPhase = "bad" - -func SetRunPhase(phase RunPhase) { - rp = phase -} - -func GetRunPhase() RunPhase { - return rp -} - -func (rp RunPhase) String() string { - return string(rp) -} - func AssertGoBuild(args []string) { if len(args) < 2 { util.Assert(false, "empty go build command") @@ -103,7 +80,7 @@ func IsCompileCommand(line string) bool { } func GetTempBuildDir() string { - return filepath.Join(TempBuildDir, rp.String()) + return filepath.Join(TempBuildDir, util.GetRunPhase().String()) } func GetTempBuildDirWith(name string) string { @@ -115,15 +92,15 @@ func GetLogPath(name string) string { } func GetInstrumentLogPath(name string) string { - return filepath.Join(TempBuildDir, PInstrument, name) + return filepath.Join(TempBuildDir, util.PInstrument, name) } func GetPreprocessLogPath(name string) string { - return filepath.Join(TempBuildDir, PPreprocess, name) + return filepath.Join(TempBuildDir, util.PPreprocess, name) } func GetConfigureLogPath(name string) string { - return filepath.Join(TempBuildDir, PConfigure, name) + return filepath.Join(TempBuildDir, util.PConfigure, name) } func GetVarNameOfFunc(fn string) string { @@ -163,7 +140,7 @@ func GetGoModPath() (string, error) { cmd := exec.Command("go", "env", "GOMOD") out, err := cmd.CombinedOutput() if err != nil { - return "", fmt.Errorf("failed to get go.mod directory: %w\n%v", + return "", fmt.Errorf("failed to check go.mod existence: %w\n%v", err, string(out)) } path := strings.TrimSpace(string(out)) @@ -239,30 +216,6 @@ func MakePublic(name string) string { return strings.Title(name) } -func InPreprocess() bool { - return rp == PPreprocess -} - -func InInstrument() bool { - return rp == PInstrument -} - -func InConfigure() bool { - return rp == PConfigure -} - -func GuaranteeInPreprocess() { - util.Assert(rp == PPreprocess, "not in preprocess stage") -} - -func GuaranteeInInstrument() { - util.Assert(rp == PInstrument, "not in instrument stage") -} - -func GuaranteeInConfigure() { - util.Assert(rp == PConfigure, "not in configure stage") -} - // splitVersionRange splits the version range into two parts, start and end. func splitVersionRange(vr string) (string, string) { util.Assert(strings.Contains(vr, ","), "invalid version range format") diff --git a/tool/util/log.go b/tool/util/log.go new file mode 100644 index 00000000..31fc3d1f --- /dev/null +++ b/tool/util/log.go @@ -0,0 +1,45 @@ +// Copyright (c) 2024 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "fmt" + "os" +) + +var logWriter *os.File = os.Stdout + +var Guarantee = Assert // More meaningful name:) + +func SetLogTo(w *os.File) { + logWriter = w +} + +func Log(format string, args ...interface{}) { + template := "[" + GetRunPhase().String() + "] " + format + "\n" + fmt.Fprintf(logWriter, template, args...) +} + +func LogFatal(format string, args ...interface{}) { + // Log errors to debug file + Log(format, args...) + // And print to stderr then, in red color + template := "Build error:\033[31m\n" + format + "\033[0m\n" + fmt.Fprintf(os.Stderr, template, + args...) + fmt.Fprintf(os.Stderr, "See build log %s for details.\n", + logWriter.Name()) + os.Exit(1) +} diff --git a/tool/util/util.go b/tool/util/util.go index e2fa1032..8694d2c7 100644 --- a/tool/util/util.go +++ b/tool/util/util.go @@ -18,7 +18,6 @@ import ( "fmt" "io" "io/ioutil" - "log" "math/rand" "os" "os/exec" @@ -28,7 +27,52 @@ import ( "time" ) -var Guarantee = Assert // More meaningful name:) +type RunPhase string + +const ( + PInvalid = "invalid" + PPreprocess = "preprocess" + PInstrument = "instrument" + PConfigure = "configure" +) + +var rp RunPhase = "bad" + +func SetRunPhase(phase RunPhase) { + rp = phase +} + +func GetRunPhase() RunPhase { + return rp +} + +func (rp RunPhase) String() string { + return string(rp) +} + +func InPreprocess() bool { + return rp == PPreprocess +} + +func InInstrument() bool { + return rp == PInstrument +} + +func InConfigure() bool { + return rp == PConfigure +} + +func GuaranteeInPreprocess() { + Assert(rp == PPreprocess, "not in preprocess stage") +} + +func GuaranteeInInstrument() { + Assert(rp == PInstrument, "not in instrument stage") +} + +func GuaranteeInConfigure() { + Assert(rp == PConfigure, "not in configure stage") +} func Assert(cond bool, format string, args ...interface{}) { if !cond { @@ -97,7 +141,7 @@ func CopyFile(src, dst string) error { defer func(sourceFile *os.File) { err := sourceFile.Close() if err != nil { - log.Fatalf("failed to close file %s: %v", sourceFile.Name(), err) + LogFatal("failed to close file %s: %v", sourceFile.Name(), err) } }(sourceFile) @@ -108,7 +152,7 @@ func CopyFile(src, dst string) error { defer func(destFile *os.File) { err := destFile.Close() if err != nil { - log.Fatalf("failed to close file %s: %v", destFile.Name(), err) + LogFatal("failed to close file %s: %v", destFile.Name(), err) } }(destFile) @@ -127,7 +171,7 @@ func ReadFile(filePath string) (string, error) { defer func(file *os.File) { err := file.Close() if err != nil { - log.Fatalf("failed to close file %s: %v", file.Name(), err) + LogFatal("failed to close file %s: %v", file.Name(), err) } }(file) @@ -148,7 +192,7 @@ func WriteFile(filePath string, content string) (string, error) { defer func(file *os.File) { err := file.Close() if err != nil { - log.Fatalf("failed to close file %s: %v", file.Name(), err) + LogFatal("failed to close file %s: %v", file.Name(), err) } }(file) @@ -165,6 +209,10 @@ func ListFiles(dir string) ([]string, error) { if err != nil { return err } + // Dont list files under hidden directories + if strings.HasPrefix(info.Name(), ".") { + return filepath.SkipDir + } if !info.IsDir() { files = append(files, path) } @@ -248,7 +296,7 @@ func IsUnix() bool { func PhaseTimer(name string) func() { start := time.Now() return func() { - log.Printf("%s took %f s", name, time.Since(start).Seconds()) + Log("%s took %f s", name, time.Since(start).Seconds()) } } @@ -265,3 +313,13 @@ func StringDedup(s []string) []string { } return r } + +func GetToolName() string { + // Get the path of the current executable + ex, err := os.Executable() + if err != nil { + LogFatal("failed to get executable: %v", err) + os.Exit(0) + } + return filepath.Base(ex) +}