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
5 changes: 2 additions & 3 deletions api/cmd/api/main.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package main

import (
"log"

"github.com/julsemaan/anyfile-notepad/api/internal/app"
"github.com/julsemaan/anyfile-notepad/api/internal/logging"
)

func main() {
if err := app.Run(app.LoadConfigFromEnv()); err != nil {
log.Fatal(err)
logging.Fatalf("%v", err)
}
}
5 changes: 3 additions & 2 deletions api/internal/app/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package app

import (
"crypto/tls"
"fmt"
"net"
"net/smtp"
"os"
"strconv"
"strings"

"github.com/julsemaan/anyfile-notepad/api/internal/logging"
)

var smtpSendMail = smtp.SendMail
Expand Down Expand Up @@ -37,7 +38,7 @@ func sendEmailWithOptionalTLS(to []string, msg []byte) error {
err = smtpSendMail(addr, auth, from, to, msg)
}
if err != nil {
fmt.Println("ERROR: Unable to send email:", err)
logging.Error("Unable to send email:", err)
return err
}

Expand Down
3 changes: 2 additions & 1 deletion api/internal/app/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/julsemaan/anyfile-notepad/api/internal/contact"
"github.com/julsemaan/anyfile-notepad/api/internal/httpapi"
"github.com/julsemaan/anyfile-notepad/api/internal/logging"
"github.com/julsemaan/anyfile-notepad/api/internal/resources"
"github.com/julsemaan/anyfile-notepad/api/internal/stats"
cache "github.com/patrickmn/go-cache"
Expand All @@ -22,7 +23,7 @@ func Run(cfg Config) error {

statsConn, err := statsd.New(statsd.Address(cfg.StatsdAddress))
if err != nil {
log.Printf("warning: statsd initialization failed: %v", err)
logging.Errorf("statsd initialization failed: %v", err)
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

This error is logged but the application continues to run without StatsD (statsConn can be nil). The updated message drops the previous “warning”/non-fatal signal, which can be misleading operationally; consider including explicit wording like “continuing without metrics” (while still keeping the required "ERROR:" prefix).

Suggested change
logging.Errorf("statsd initialization failed: %v", err)
logging.Errorf("ERROR: statsd initialization failed: %v; continuing without metrics", err)

Copilot uses AI. Check for mistakes.
}
if statsConn != nil {
defer statsConn.Close()
Expand Down
8 changes: 4 additions & 4 deletions api/internal/contact/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import (
"context"
"errors"
"fmt"
"log"
"strings"
"text/template"

"github.com/julsemaan/anyfile-notepad/api/internal/logging"
"github.com/rs/rest-layer/resource"
)

Expand Down Expand Up @@ -58,11 +58,11 @@ func (s *Service) BeforeInsert(_ context.Context, items []*resource.Item) error
for _, item := range items {
msg, buildErr := buildMessage(recipients, item)
if buildErr != nil {
log.Printf("Unable to build contact request email: %v", buildErr)
logging.Errorf("Unable to build contact request email: %v", buildErr)
return errUnableToSendEmail
}
if sendErr := s.sendEmail(recipients, msg); sendErr != nil {
log.Printf("Unable to send contact request email: %v", sendErr)
logging.Errorf("Unable to send contact request email: %v", sendErr)
return errUnableToSendEmail
}
}
Expand All @@ -72,7 +72,7 @@ func (s *Service) BeforeInsert(_ context.Context, items []*resource.Item) error

func (s *Service) AfterInsert(_ context.Context, items []*resource.Item, err *error) {
if err != nil && *err != nil {
log.Println("Not sending contact request email because there was an error saving the contact request")
logging.Error("Not sending contact request email because there was an error saving the contact request")
return
}

Expand Down
33 changes: 33 additions & 0 deletions api/internal/logging/logging.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package logging

import (
"fmt"
"log"
"os"
"strings"
)

const errorPrefix = "ERROR:"

func output(message string) {
_ = log.Default().Output(3, message)
}

func Error(args ...interface{}) {
msg := strings.TrimSuffix(fmt.Sprintln(args...), "\n")
if msg == "" {
output(errorPrefix)
return
}

output(errorPrefix + " " + msg)
}

func Errorf(format string, args ...interface{}) {
output(errorPrefix + " " + fmt.Sprintf(format, args...))
}

func Fatalf(format string, args ...interface{}) {
output(errorPrefix + " " + fmt.Sprintf(format, args...))
os.Exit(1)
}
Comment on lines +16 to +33
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

The new logging package doesn’t have any unit tests to assert the required "ERROR:" prefix behavior. Since other internal packages are covered by tests, consider adding a small test that captures log output and verifies that Error/Errorf/Fatalf all emit messages containing the standardized prefix.

Copilot uses AI. Check for mistakes.
81 changes: 81 additions & 0 deletions api/internal/logging/logging_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package logging

import (
"bytes"
"errors"
"log"
"os"
"os/exec"
"strings"
"testing"
)

func captureLogOutput(t *testing.T) *bytes.Buffer {
t.Helper()

buf := &bytes.Buffer{}
originalWriter := log.Writer()
originalFlags := log.Flags()
originalPrefix := log.Prefix()

log.SetOutput(buf)
log.SetFlags(0)
log.SetPrefix("")

t.Cleanup(func() {
log.SetOutput(originalWriter)
log.SetFlags(originalFlags)
log.SetPrefix(originalPrefix)
})

return buf
}

func TestErrorAddsPrefix(t *testing.T) {
buf := captureLogOutput(t)

Error("unable to send email", 42)

if got := buf.String(); !strings.Contains(got, "ERROR: unable to send email 42") {
t.Fatalf("expected ERROR prefix in output, got %q", got)
}
}

func TestErrorfAddsPrefix(t *testing.T) {
buf := captureLogOutput(t)

Errorf("unable to send email %d", 42)

if got := buf.String(); !strings.Contains(got, "ERROR: unable to send email 42") {
t.Fatalf("expected ERROR prefix in output, got %q", got)
}
}

func TestFatalfAddsPrefixAndExits(t *testing.T) {
if os.Getenv("LOGGING_FATALF_TEST") == "1" {
log.SetOutput(os.Stderr)
log.SetFlags(0)
log.SetPrefix("")
Fatalf("unable to send email %d", 42)
return
}
Comment on lines +55 to +61
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

TestFatalfAddsPrefixAndExits will take the helper-process branch (and call os.Exit(1)) if LOGGING_FATALF_TEST is already set in the caller’s environment. To make the test robust, explicitly clear/override this env var in the parent process (e.g., via t.Setenv before the getenv check) and only set it to "1" for the exec’d child process.

Copilot uses AI. Check for mistakes.

cmd := exec.Command(os.Args[0], "-test.run=TestFatalfAddsPrefixAndExits")
cmd.Env = append(os.Environ(), "LOGGING_FATALF_TEST=1")

var stderr bytes.Buffer
cmd.Stderr = &stderr

err := cmd.Run()
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("expected process exit error, got %v", err)
}
if exitErr.ExitCode() != 1 {
t.Fatalf("expected exit code 1, got %d", exitErr.ExitCode())
}

if got := stderr.String(); !strings.Contains(got, "ERROR: unable to send email 42") {
t.Fatalf("expected ERROR prefix in fatal output, got %q", got)
}
}
Loading