cmd/vulnreport: move all logging logic to one file

Move all logic for vulnreport logging into a new inner package, log, which
will allow us to more easily change the logging internals (e.g., if we
want to use slog in the future).

Change-Id: I8287fc186451e6dc2e846ed071bd3c6fefdad359
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/559600
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/cmd/vulnreport/create.go b/cmd/vulnreport/create.go
index 1f4245f..b25d94c 100644
--- a/cmd/vulnreport/create.go
+++ b/cmd/vulnreport/create.go
@@ -12,6 +12,7 @@
 	"strconv"
 	"strings"
 
+	"golang.org/x/vulndb/cmd/vulnreport/log"
 	"golang.org/x/vulndb/internal/cveclient"
 	"golang.org/x/vulndb/internal/cveschema5"
 	"golang.org/x/vulndb/internal/derrors"
@@ -51,11 +52,11 @@
 		return err
 	}
 
-	outlog.Println(filename)
+	log.Out(filename)
 
 	xrefs := xref(filename, r, cfg.existingByFile)
 	if len(xrefs) != 0 {
-		infolog.Printf("found cross-references:\n%s", xrefs)
+		log.Infof("found cross-references:\n%s", xrefs)
 	}
 
 	return nil
@@ -75,7 +76,7 @@
 		if err != nil {
 			return err
 		}
-		infolog.Printf("found %d issues with label %s\n", len(tempIssues), label)
+		log.Infof("found %d issues with label %s\n", len(tempIssues), label)
 		isses = append(isses, tempIssues...)
 	}
 
@@ -83,13 +84,13 @@
 	for _, iss := range isses {
 		// Don't create a report for an issue that already has a report.
 		if _, ok := cfg.existingByIssue[iss.Number]; ok {
-			infolog.Printf("skipped issue %d which already has a report\n", iss.Number)
+			log.Infof("skipped issue %d which already has a report\n", iss.Number)
 			continue
 		}
 
 		r, err := createReport(ctx, cfg, iss)
 		if err != nil {
-			errlog.Printf("skipped issue %d: %v\n", iss.Number, err)
+			log.Errf("skipped issue %d: %v\n", iss.Number, err)
 			continue
 		}
 
@@ -103,11 +104,11 @@
 
 	skipped := len(isses) - len(created)
 	if skipped > 0 {
-		infolog.Printf("skipped %d issue(s)\n", skipped)
+		log.Infof("skipped %d issue(s)\n", skipped)
 	}
 
 	if len(created) == 0 {
-		infolog.Printf("no files to commit, exiting")
+		log.Infof("no files to commit, exiting")
 		return nil
 	}
 
@@ -219,13 +220,13 @@
 
 	aliases := allAliases(ctx, parsed.aliases, cfg.ghsaClient)
 	if alias, ok := pickBestAlias(aliases, *preferCVE); ok {
-		infolog.Printf("creating report %s based on %s (picked from [%s])", parsed.id, alias, strings.Join(aliases, ", "))
+		log.Infof("creating report %s based on %s (picked from [%s])", parsed.id, alias, strings.Join(aliases, ", "))
 		r, err = reportFromAlias(ctx, parsed.id, parsed.modulePath, alias, cfg)
 		if err != nil {
 			return nil, err
 		}
 	} else {
-		infolog.Printf("no alias found, creating basic report for %s", parsed.id)
+		log.Infof("no alias found, creating basic report for %s", parsed.id)
 		r = &report.Report{
 			ID: parsed.id,
 			Modules: []*report.Module{
@@ -258,11 +259,11 @@
 	if cfg.aiClient != nil {
 		suggestions, err := suggest(ctx, cfg.aiClient, r, 1)
 		if err != nil {
-			warnlog.Printf("failed to get AI-generated suggestions for %s: %v\n", r.ID, err)
+			log.Warnf("failed to get AI-generated suggestions for %s: %v\n", r.ID, err)
 		} else if len(suggestions) == 0 {
-			warnlog.Printf("failed to get AI-generated suggestions for %s (none generated)\n", r.ID)
+			log.Warnf("failed to get AI-generated suggestions for %s (none generated)\n", r.ID)
 		} else {
-			infolog.Printf("applying AI-generated suggestion for %s", r.ID)
+			log.Infof("applying AI-generated suggestion for %s", r.ID)
 			applySuggestion(r, suggestions[0])
 		}
 	}
@@ -313,7 +314,7 @@
 	}
 
 	if len(parsed.aliases) == 0 {
-		infolog.Printf("%q has no CVE or GHSA IDs\n", iss.Title)
+		log.Infof("%q has no CVE or GHSA IDs\n", iss.Title)
 	}
 
 	return parsed, nil
@@ -373,13 +374,13 @@
 		if err != nil {
 			// If a CVE is not found, it is most likely a CVE we reserved but haven't
 			// published yet.
-			infolog.Printf("no published record found for %s, creating basic report", alias)
+			log.Infof("no published record found for %s, creating basic report", alias)
 			return basicReport(id, modulePath), nil
 		}
 		return report.CVE5ToReport(cve, id, modulePath, cfg.proxyClient), nil
 	}
 
-	infolog.Printf("alias %s is not a CVE or GHSA, creating basic report", alias)
+	log.Infof("alias %s is not a CVE or GHSA, creating basic report", alias)
 	return basicReport(id, modulePath), nil
 }
 
diff --git a/cmd/vulnreport/cve.go b/cmd/vulnreport/cve.go
index bce76d0..ba4cbb2 100644
--- a/cmd/vulnreport/cve.go
+++ b/cmd/vulnreport/cve.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/vulndb/cmd/vulnreport/log"
 	"golang.org/x/vulndb/internal/database"
 	"golang.org/x/vulndb/internal/derrors"
 	"golang.org/x/vulndb/internal/report"
@@ -22,7 +23,7 @@
 		if err := writeCVE(r); err != nil {
 			return err
 		}
-		outlog.Println(r.CVEFilename())
+		log.Out(r.CVEFilename())
 	}
 	return nil
 }
diff --git a/cmd/vulnreport/find_aliases.go b/cmd/vulnreport/find_aliases.go
index 3139843..dbaf983 100644
--- a/cmd/vulnreport/find_aliases.go
+++ b/cmd/vulnreport/find_aliases.go
@@ -9,6 +9,7 @@
 	"fmt"
 
 	"golang.org/x/exp/slices"
+	"golang.org/x/vulndb/cmd/vulnreport/log"
 	"golang.org/x/vulndb/internal/cveschema5"
 	"golang.org/x/vulndb/internal/ghsa"
 	"golang.org/x/vulndb/internal/report"
@@ -68,7 +69,7 @@
 		all = append(all, alias)
 		aliases, err := aliasesFor(ctx, alias)
 		if err != nil {
-			errlog.Printf(err.Error())
+			log.Err(err)
 			continue
 		}
 		queue = append(queue, aliases...)
diff --git a/cmd/vulnreport/fix.go b/cmd/vulnreport/fix.go
index 1ad8b68..5f5ce9d 100644
--- a/cmd/vulnreport/fix.go
+++ b/cmd/vulnreport/fix.go
@@ -14,6 +14,7 @@
 
 	"github.com/google/go-cmp/cmp"
 	"golang.org/x/exp/slices"
+	"golang.org/x/vulndb/cmd/vulnreport/log"
 	"golang.org/x/vulndb/internal/derrors"
 	"golang.org/x/vulndb/internal/ghsa"
 	"golang.org/x/vulndb/internal/osvutils"
@@ -30,7 +31,7 @@
 
 func fix(ctx context.Context, filename string, ghsaClient *ghsa.Client, pc *proxy.Client, force bool) (err error) {
 	defer derrors.Wrap(&err, "fix(%q)", filename)
-	infolog.Printf("fix %s\n", filename)
+	log.Infof("fix %s\n", filename)
 
 	r, err := report.Read(filename)
 	if err != nil {
@@ -44,7 +45,7 @@
 	// report even if a fatal error occurs somewhere.
 	defer func() {
 		if err := r.Write(filename); err != nil {
-			errlog.Println(err)
+			log.Err(err)
 		}
 	}()
 
@@ -52,19 +53,19 @@
 		r.Fix(pc)
 	}
 	if lints := r.Lint(pc); len(lints) > 0 {
-		warnlog.Printf("%s still has lint errors after fix:\n\t- %s", filename, strings.Join(lints, "\n\t- "))
+		log.Warnf("%s still has lint errors after fix:\n\t- %s", filename, strings.Join(lints, "\n\t- "))
 	}
 
 	if !*skipSymbols {
-		infolog.Printf("%s: checking packages and symbols (use -skip-symbols to skip this)", r.ID)
+		log.Infof("%s: checking packages and symbols (use -skip-symbols to skip this)", r.ID)
 		if err := checkReportSymbols(r); err != nil {
 			return err
 		}
 	}
 	if !*skipAlias {
-		infolog.Printf("%s: checking for missing GHSAs and CVEs (use -skip-alias to skip this)", r.ID)
+		log.Infof("%s: checking for missing GHSAs and CVEs (use -skip-alias to skip this)", r.ID)
 		if added := addMissingAliases(ctx, r, ghsaClient); added > 0 {
-			infolog.Printf("%s: added %d missing aliases", r.ID, added)
+			log.Infof("%s: added %d missing aliases", r.ID, added)
 		}
 	}
 
@@ -85,7 +86,7 @@
 
 func checkReportSymbols(r *report.Report) error {
 	if r.IsExcluded() {
-		infolog.Printf("%s is excluded, skipping symbol checks\n", r.ID)
+		log.Infof("%s is excluded, skipping symbol checks\n", r.ID)
 		return nil
 	}
 	for _, m := range r.Modules {
@@ -100,20 +101,20 @@
 				return err
 			}
 			if ver == "" || !affected {
-				warnlog.Printf("%s: current Go version %q is not in a vulnerable range, skipping symbol checks for module %s\n", r.ID, gover, m.Module)
+				log.Warnf("%s: current Go version %q is not in a vulnerable range, skipping symbol checks for module %s\n", r.ID, gover, m.Module)
 				continue
 			}
 			if ver != m.VulnerableAt {
-				warnlog.Printf("%s: current Go version %q does not match vulnerable_at version (%s) for module %s\n", r.ID, ver, m.VulnerableAt, m.Module)
+				log.Warnf("%s: current Go version %q does not match vulnerable_at version (%s) for module %s\n", r.ID, ver, m.VulnerableAt, m.Module)
 			}
 		}
 
 		for _, p := range m.Packages {
 			if p.SkipFix != "" {
-				infolog.Printf("%s: skipping symbol checks for package %s (reason: %q)\n", r.ID, p.Package, p.SkipFix)
+				log.Infof("%s: skipping symbol checks for package %s (reason: %q)\n", r.ID, p.Package, p.SkipFix)
 				continue
 			}
-			syms, err := symbols.Exported(m, p, errlog)
+			syms, err := symbols.Exported(m, p, log.Errf, log.Err)
 			if err != nil {
 				return fmt.Errorf("package %s: %w", p.Package, err)
 			}
@@ -121,7 +122,7 @@
 			syms = removeExcluded(syms, p.ExcludedSymbols)
 			if !cmp.Equal(syms, p.DerivedSymbols) {
 				p.DerivedSymbols = syms
-				infolog.Printf("%s: updated derived symbols for package %s\n", r.ID, p.Package)
+				log.Infof("%s: updated derived symbols for package %s\n", r.ID, p.Package)
 			}
 		}
 	}
@@ -136,7 +137,7 @@
 	var newSyms []string
 	for _, d := range syms {
 		if slices.Contains(excluded, d) {
-			infolog.Printf("removed excluded symbol %s\n", d)
+			log.Infof("removed excluded symbol %s\n", d)
 			continue
 		}
 		newSyms = append(newSyms, d)
diff --git a/cmd/vulnreport/lint.go b/cmd/vulnreport/lint.go
index 1586800..cd203e3 100644
--- a/cmd/vulnreport/lint.go
+++ b/cmd/vulnreport/lint.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 
+	"golang.org/x/vulndb/cmd/vulnreport/log"
 	"golang.org/x/vulndb/internal/derrors"
 	"golang.org/x/vulndb/internal/proxy"
 	"golang.org/x/vulndb/internal/report"
@@ -14,7 +15,7 @@
 
 func lint(_ context.Context, filename string, pc *proxy.Client) (err error) {
 	defer derrors.Wrap(&err, "lint(%q)", filename)
-	infolog.Printf("lint %s\n", filename)
+	log.Infof("lint %s\n", filename)
 
 	_, err = report.ReadAndLint(filename, pc)
 	return err
diff --git a/cmd/vulnreport/log/log.go b/cmd/vulnreport/log/log.go
new file mode 100644
index 0000000..bf60bc7
--- /dev/null
+++ b/cmd/vulnreport/log/log.go
@@ -0,0 +1,62 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package log
+
+import (
+	"io"
+	"os"
+
+	"log"
+)
+
+var (
+	infolog *log.Logger
+	outlog  *log.Logger
+	warnlog *log.Logger
+	errlog  *log.Logger
+)
+
+func Init(quiet bool) {
+	if quiet {
+		infolog = log.New(io.Discard, "", 0)
+	} else {
+		infolog = log.New(os.Stderr, "info: ", 0)
+	}
+	outlog = log.New(os.Stdout, "", 0)
+	warnlog = log.New(os.Stderr, "WARNING: ", 0)
+	errlog = log.New(os.Stderr, "ERROR: ", 0)
+}
+
+func Infof(format string, v ...any) {
+	infolog.Printf(format, v...)
+}
+
+func Outf(format string, v ...any) {
+	outlog.Printf(format, v...)
+}
+
+func Warnf(format string, v ...any) {
+	warnlog.Printf(format, v...)
+}
+
+func Errf(format string, v ...any) {
+	errlog.Printf(format, v...)
+}
+
+func Info(v ...any) {
+	infolog.Println(v...)
+}
+
+func Out(v ...any) {
+	outlog.Println(v...)
+}
+
+func Warn(v ...any) {
+	warnlog.Println(v...)
+}
+
+func Err(v ...any) {
+	errlog.Println(v...)
+}
diff --git a/cmd/vulnreport/main.go b/cmd/vulnreport/main.go
index e8033ce..28cc9ee 100644
--- a/cmd/vulnreport/main.go
+++ b/cmd/vulnreport/main.go
@@ -10,12 +10,12 @@
 	"context"
 	"flag"
 	"fmt"
-	"io"
 	"log"
 	"os"
 	"path/filepath"
 	"runtime/pprof"
 
+	vlog "golang.org/x/vulndb/cmd/vulnreport/log"
 	"golang.org/x/vulndb/internal/ghsa"
 	"golang.org/x/vulndb/internal/gitrepo"
 	"golang.org/x/vulndb/internal/proxy"
@@ -28,18 +28,8 @@
 	quiet       = flag.Bool("q", false, "quiet mode (suppress info logs)")
 )
 
-var (
-	infolog *log.Logger
-	outlog  *log.Logger
-	warnlog *log.Logger
-	errlog  *log.Logger
-)
-
 func init() {
-	infolog = log.New(os.Stdout, "info: ", 0)
-	outlog = log.New(os.Stdout, "", 0)
-	warnlog = log.New(os.Stderr, "WARNING: ", 0)
-	errlog = log.New(os.Stderr, "ERROR: ", 0)
+	vlog.Init(*quiet)
 }
 
 func main() {
@@ -70,10 +60,6 @@
 		*githubToken = os.Getenv("VULN_GITHUB_ACCESS_TOKEN")
 	}
 
-	if *quiet {
-		infolog = log.New(io.Discard, "", 0)
-	}
-
 	var (
 		args []string
 		cmd  = flag.Arg(0)
@@ -114,7 +100,7 @@
 			// instead of filenames.
 			for _, githubID := range githubIDs {
 				if err := create(ctx, githubID, cfg); err != nil {
-					errlog.Println(err)
+					vlog.Err(err)
 				}
 			}
 		}
@@ -163,8 +149,8 @@
 			if err != nil {
 				return err
 			}
-			outlog.Println(name)
-			outlog.Println(xref(name, r, existingByFile))
+			vlog.Out(name)
+			vlog.Out(xref(name, r, existingByFile))
 			return nil
 		}
 	default:
@@ -176,11 +162,11 @@
 	for _, arg := range args {
 		arg, err := argToFilename(arg)
 		if err != nil {
-			errlog.Println(err)
+			vlog.Err(err)
 			continue
 		}
 		if err := cmdFunc(ctx, arg); err != nil {
-			errlog.Println(err)
+			vlog.Err(err)
 		}
 	}
 }
diff --git a/cmd/vulnreport/osv.go b/cmd/vulnreport/osv.go
index 24724bb..091df84 100644
--- a/cmd/vulnreport/osv.go
+++ b/cmd/vulnreport/osv.go
@@ -8,6 +8,7 @@
 	"context"
 	"time"
 
+	"golang.org/x/vulndb/cmd/vulnreport/log"
 	"golang.org/x/vulndb/internal/database"
 	"golang.org/x/vulndb/internal/derrors"
 	"golang.org/x/vulndb/internal/proxy"
@@ -25,7 +26,7 @@
 		if err := writeOSV(r); err != nil {
 			return err
 		}
-		outlog.Println(r.OSVFilename())
+		log.Out(r.OSVFilename())
 	}
 	return nil
 }
diff --git a/cmd/vulnreport/suggest.go b/cmd/vulnreport/suggest.go
index 55e59f0..f32052f 100644
--- a/cmd/vulnreport/suggest.go
+++ b/cmd/vulnreport/suggest.go
@@ -9,6 +9,7 @@
 	"flag"
 	"fmt"
 
+	"golang.org/x/vulndb/cmd/vulnreport/log"
 	"golang.org/x/vulndb/internal/derrors"
 	"golang.org/x/vulndb/internal/genai"
 	"golang.org/x/vulndb/internal/report"
@@ -27,7 +28,7 @@
 		return err
 	}
 
-	infolog.Print("contacting the Gemini API...")
+	log.Info("contacting the Gemini API...")
 	c, err := genai.NewGeminiClient(ctx)
 	if err != nil {
 		return err
@@ -39,10 +40,10 @@
 	}
 	found := len(suggestions)
 
-	outlog.Printf("== AI-generated suggestions for report %s ==\n", r.ID)
+	log.Outf("== AI-generated suggestions for report %s ==\n", r.ID)
 
 	for i, s := range suggestions {
-		outlog.Printf("\nSuggestion %d/%d\nsummary: %s\ndescription: %s\n",
+		log.Outf("\nSuggestion %d/%d\nsummary: %s\ndescription: %s\n",
 			i+1, found, s.Summary, s.Description)
 
 		// In interactive mode, allow user to accept the suggestion,
@@ -51,9 +52,9 @@
 		// instead of upfront.
 		if *interactive {
 			if i == found-1 {
-				outlog.Printf("\naccept or quit? (a=accept/Q=quit) ")
+				log.Outf("\naccept or quit? (a=accept/Q=quit) ")
 			} else {
-				outlog.Printf("\naccept, see next suggestion, or quit? (a=accept/n=next/Q=quit) ")
+				log.Outf("\naccept, see next suggestion, or quit? (a=accept/n=next/Q=quit) ")
 			}
 
 			var choice string
@@ -64,7 +65,7 @@
 			case "a":
 				applySuggestion(r, s)
 				if err := r.Write(filename); err != nil {
-					errlog.Println(err)
+					log.Err(err)
 				}
 				return nil
 			case "n":
diff --git a/cmd/vulnreport/symbols.go b/cmd/vulnreport/symbols.go
index 0483cb6..59c85b1 100644
--- a/cmd/vulnreport/symbols.go
+++ b/cmd/vulnreport/symbols.go
@@ -10,6 +10,7 @@
 	"path/filepath"
 	"strings"
 
+	"golang.org/x/vulndb/cmd/vulnreport/log"
 	"golang.org/x/vulndb/internal/derrors"
 	"golang.org/x/vulndb/internal/osv"
 	"golang.org/x/vulndb/internal/report"
@@ -18,7 +19,7 @@
 
 func findSymbols(_ context.Context, filename string) (err error) {
 	defer derrors.Wrap(&err, "findSymbols(%q)", filename)
-	infolog.Printf("symbols %s\n", filename)
+	log.Infof("symbols %s\n", filename)
 
 	r, err := report.Read(filename)
 	if err != nil {
@@ -46,9 +47,9 @@
 		for i, fixLink := range defaultFixes {
 			fixHash := filepath.Base(fixLink)
 			fixRepo := strings.TrimSuffix(fixLink, "/commit/"+fixHash)
-			pkgsToSymbols, err := symbols.Patched(mod.Module, fixRepo, fixHash, errlog)
+			pkgsToSymbols, err := symbols.Patched(mod.Module, fixRepo, fixHash, log.Errf)
 			if err != nil {
-				errlog.Print(err)
+				log.Err(err)
 				continue
 			}
 			packages := mod.AllPackages()
diff --git a/internal/symbols/exported_functions.go b/internal/symbols/exported_functions.go
index 180cffb..b1ea606 100644
--- a/internal/symbols/exported_functions.go
+++ b/internal/symbols/exported_functions.go
@@ -8,7 +8,6 @@
 	"bytes"
 	"fmt"
 	"go/types"
-	"log"
 	"os"
 	"os/exec"
 	"sort"
@@ -24,7 +23,7 @@
 
 // Exported returns a set of vulnerable symbols, in the vuln
 // db format, exported by a package p from the module m.
-func Exported(m *report.Module, p *report.Package, errlog *log.Logger) (_ []string, err error) {
+func Exported(m *report.Module, p *report.Package, errf logf, errln logln) (_ []string, err error) {
 	defer derrors.Wrap(&err, "Exported(%q, %q)", m.Module, p.Package)
 
 	cleanup, err := changeToTempDir()
@@ -37,7 +36,7 @@
 		cmd := exec.Command(name, arg...)
 		out, err := cmd.CombinedOutput()
 		if err != nil {
-			errlog.Println(string(out))
+			errln(string(out))
 		}
 		return err
 	}
@@ -116,17 +115,17 @@
 		if typ, method, ok := strings.Cut(sym, "."); ok {
 			n, ok := pkg.Types.Scope().Lookup(typ).(*types.TypeName)
 			if !ok {
-				errlog.Printf("package %s: %v: type not found\n", p.Package, typ)
+				errf("package %s: %v: type not found\n", p.Package, typ)
 				continue
 			}
 			m, _, _ := types.LookupFieldOrMethod(n.Type(), true, pkg.Types, method)
 			if m == nil {
-				errlog.Printf("package %s: %v: method not found\n", p.Package, sym)
+				errf("package %s: %v: method not found\n", p.Package, sym)
 			}
 		} else {
 			_, ok := pkg.Types.Scope().Lookup(typ).(*types.Func)
 			if !ok {
-				errlog.Printf("package %s: %v: func not found\n", p.Package, typ)
+				errf("package %s: %v: func not found\n", p.Package, typ)
 			}
 		}
 	}
@@ -154,6 +153,11 @@
 	return newslice, nil
 }
 
+// TODO(tatianabradley): Refactor functions that use these to return
+// info needed to construct logs instead of logging directly.
+type logf func(format string, v ...any)
+type logln func(v ...any)
+
 // exportedFunctions returns a set of vulnerable functions exported
 // by a packages from the module.
 func exportedFunctions(pkg *packages.Package, m *report.Module) (_ map[string]bool, err error) {
diff --git a/internal/symbols/patched_functions.go b/internal/symbols/patched_functions.go
index 039c988..7704db4 100644
--- a/internal/symbols/patched_functions.go
+++ b/internal/symbols/patched_functions.go
@@ -14,7 +14,6 @@
 	"go/printer"
 	"go/token"
 	"io/fs"
-	"log"
 	"os"
 	"path"
 	"path/filepath"
@@ -36,7 +35,7 @@
 // patched in the package. Test packages and symbols are omitted.
 //
 // If the commit has more than one parent, an error is returned.
-func Patched(module, repoURL, commitHash string, errlog *log.Logger) (_ map[string][]string, err error) {
+func Patched(module, repoURL, commitHash string, errf logf) (_ map[string][]string, err error) {
 	defer derrors.Wrap(&err, "Patched(%s, %s, %s)", module, repoURL, commitHash)
 
 	repoRoot, err := os.MkdirTemp("", commitHash)
@@ -91,7 +90,7 @@
 		return nil, err
 	}
 
-	patched := patchedSymbols(oldSymbols, newSymbols, errlog)
+	patched := patchedSymbols(oldSymbols, newSymbols, errf)
 	pkgSyms := make(map[string][]string)
 	for _, sym := range patched {
 		pkgSyms[sym.pkg] = append(pkgSyms[sym.pkg], sym.symbol)
@@ -102,7 +101,7 @@
 // patchedSymbols returns symbol indices in oldSymbols that either 1) cannot
 // be identified in newSymbols or 2) the corresponding functions have their
 // source code changed.
-func patchedSymbols(oldSymbols, newSymbols map[symKey]*ast.FuncDecl, errlog *log.Logger) []symKey {
+func patchedSymbols(oldSymbols, newSymbols map[symKey]*ast.FuncDecl, errf logf) []symKey {
 	var syms []symKey
 	for key, of := range oldSymbols {
 		nf, ok := newSymbols[key]
@@ -113,7 +112,7 @@
 			continue
 		}
 
-		if source(of, errlog) != source(nf, errlog) {
+		if source(of, errf) != source(nf, errf) {
 			syms = append(syms, key)
 		}
 	}
@@ -121,12 +120,12 @@
 }
 
 // source returns f's source code as text.
-func source(f *ast.FuncDecl, errlog *log.Logger) string {
+func source(f *ast.FuncDecl, errf logf) string {
 	var b bytes.Buffer
 	fs := token.NewFileSet()
 	if err := printer.Fprint(&b, fs, f); err != nil {
 		// should not happen, so just printing a warning
-		errlog.Printf("getting source of %s failed with %v", astSymbolName(f), err)
+		errf("getting source of %s failed with %v", astSymbolName(f), err)
 		return ""
 	}
 	return strings.TrimSpace(b.String())
diff --git a/internal/symbols/patched_functions_test.go b/internal/symbols/patched_functions_test.go
index 194b3b4..1dfd3d2 100644
--- a/internal/symbols/patched_functions_test.go
+++ b/internal/symbols/patched_functions_test.go
@@ -7,8 +7,6 @@
 	"go/ast"
 	"go/parser"
 	"go/token"
-	"log"
-	"os"
 	"path/filepath"
 	"sort"
 	"testing"
@@ -47,8 +45,9 @@
 		if err != nil {
 			t.Error(err)
 		}
-
-		got := toMap(patchedSymbols(oldSyms, newSyms, log.New(os.Stderr, "ERROR: ", 0)))
+		// logs are not important for this test
+		discardLog := func(string, ...any) {}
+		got := toMap(patchedSymbols(oldSyms, newSyms, discardLog))
 		if diff := cmp.Diff(got, tc.want); diff != "" {
 			t.Errorf("(-got, want+):\n%s", diff)
 		}