From f87adeff9ff0ce220c57f9a53d94804618435706 Mon Sep 17 00:00:00 2001 From: Jan Novak Date: Thu, 7 May 2026 13:59:22 +0200 Subject: [PATCH] feat(go/fio): debug logging via slog at LOG_LEVEL=DEBUG Wires slog.SetDefault to honour LOG_LEVEL in all CLI commands and adds debug logs on the Fio fetch path so a silent "fetched 0 transaction(s)" can be diagnosed without code changes: - fio.New: which client variant (api/transparent) was selected - apiClient: GET URL (token redacted as ****), HTTP status, body bytes, parsed transaction count - transparentClient: GET URL, HTTP status, body bytes, plus parser stats (raw rows from second table, kept, dropped_bad_date, dropped_nonpositive_amount) Also suppresses the --print-fio-table block when zero transactions were fetched, so the bare header no longer prints under that condition. Co-Authored-By: Claude Opus 4.7 --- go/cmd/fuj/main.go | 4 ++++ go/internal/io/fio/api.go | 10 +++++++++- go/internal/io/fio/client.go | 3 +++ go/internal/io/fio/transparent.go | 20 +++++++++++++++++++- go/internal/services/banksync/sync.go | 4 ++-- 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/go/cmd/fuj/main.go b/go/cmd/fuj/main.go index a55d3e0..dfa21a3 100644 --- a/go/cmd/fuj/main.go +++ b/go/cmd/fuj/main.go @@ -11,6 +11,7 @@ import ( "fuj-management/go/internal/services/banksync" "fuj-management/go/internal/services/membership" "fuj-management/go/internal/web" + "log/slog" "os" "time" ) @@ -28,6 +29,9 @@ func main() { os.Exit(2) } + // Honour LOG_LEVEL for slog calls in any package (e.g. internal/io/fio debug logs). + slog.SetDefault(logging.New(os.Getenv("LOG_LEVEL"))) + cmd, args := os.Args[1], os.Args[2:] switch cmd { diff --git a/go/internal/io/fio/api.go b/go/internal/io/fio/api.go index 26a8172..be32f91 100644 --- a/go/internal/io/fio/api.go +++ b/go/internal/io/fio/api.go @@ -5,7 +5,9 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" + "strings" "time" ) @@ -25,6 +27,9 @@ func (c *apiClient) FetchTransactions(ctx context.Context, from, to time.Time) ( const layout = "2006-01-02" url := fmt.Sprintf("https://fioapi.fio.cz/v1/rest/periods/%s/%s/%s/transactions.json", c.token, from.Format(layout), to.Format(layout)) + slog.Debug("fio api: GET", + "url", strings.Replace(url, c.token, "****", 1), + "from", from.Format(layout), "to", to.Format(layout)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -35,6 +40,7 @@ func (c *apiClient) FetchTransactions(ctx context.Context, from, to time.Time) ( return nil, err } defer resp.Body.Close() + slog.Debug("fio api: response", "status", resp.StatusCode) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("fio api: HTTP %d", resp.StatusCode) } @@ -42,7 +48,9 @@ func (c *apiClient) FetchTransactions(ctx context.Context, from, to time.Time) ( if err != nil { return nil, err } - return parseAPIResponse(body) + txns, err := parseAPIResponse(body) + slog.Debug("fio api: parsed", "body_bytes", len(body), "parsed_count", len(txns)) + return txns, err } // fioAPIResponse is the top-level envelope from the Fio JSON API. diff --git a/go/internal/io/fio/client.go b/go/internal/io/fio/client.go index db40d54..2230ed5 100644 --- a/go/internal/io/fio/client.go +++ b/go/internal/io/fio/client.go @@ -4,6 +4,7 @@ package fio import ( "context" + "log/slog" "net/http" "time" ) @@ -36,7 +37,9 @@ func New(token, accountNum string, hc httpDoer) Client { hc = http.DefaultClient } if token != "" { + slog.Debug("fio: client selected", "type", "api") return &apiClient{token: token, hc: hc} } + slog.Debug("fio: client selected", "type", "transparent", "account_num", accountNum) return &transparentClient{accountNum: accountNum, hc: hc} } diff --git a/go/internal/io/fio/transparent.go b/go/internal/io/fio/transparent.go index 29ff7df..ff47e08 100644 --- a/go/internal/io/fio/transparent.go +++ b/go/internal/io/fio/transparent.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "log/slog" "net/http" "regexp" "strings" @@ -28,6 +29,10 @@ func (c *transparentClient) FetchTransactions(ctx context.Context, from, to time from.Format("2.1.2006"), to.Format("2.1.2006"), ) + slog.Debug("fio transparent: GET", + "url", url, + "from", from.Format("2006-01-02"), "to", to.Format("2006-01-02")) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err @@ -37,6 +42,7 @@ func (c *transparentClient) FetchTransactions(ctx context.Context, from, to time return nil, err } defer resp.Body.Close() + slog.Debug("fio transparent: response", "status", resp.StatusCode) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("fio transparent: HTTP %d", resp.StatusCode) } @@ -44,6 +50,7 @@ func (c *transparentClient) FetchTransactions(ctx context.Context, from, to time if err != nil { return nil, err } + slog.Debug("fio transparent: body read", "body_bytes", len(body)) return parseTransparentHTML(body) } @@ -63,6 +70,7 @@ func parseTransparentHTML(body []byte) ([]Transaction, error) { rows := extractSecondTableRows(body) var txns []Transaction + var droppedBadDate, droppedNonpositive int for _, row := range rows { col := func(i int) string { if i < len(row) { @@ -72,7 +80,12 @@ func parseTransparentHTML(body []byte) ([]Transaction, error) { } dateStr := parseCzechDate(col(tColDate)) amount := parseCzechAmount(col(tColAmount)) - if dateStr == "" || amount <= 0 { + if dateStr == "" { + droppedBadDate++ + continue + } + if amount <= 0 { + droppedNonpositive++ continue } txns = append(txns, Transaction{ @@ -86,6 +99,11 @@ func parseTransparentHTML(body []byte) ([]Transaction, error) { BankID: "", // not available on HTML path }) } + slog.Debug("fio transparent: parsed", + "raw_rows", len(rows), + "kept", len(txns), + "dropped_bad_date", droppedBadDate, + "dropped_nonpositive_amount", droppedNonpositive) return txns, nil } diff --git a/go/internal/services/banksync/sync.go b/go/internal/services/banksync/sync.go index 95559d0..7a5fe6a 100644 --- a/go/internal/services/banksync/sync.go +++ b/go/internal/services/banksync/sync.go @@ -112,8 +112,8 @@ func SyncToSheets( }) } - // 4b. Optional debug table (dry-run only). - if opts.DryRun && opts.PrintFioTable { + // 4b. Optional debug table (dry-run only; suppress when nothing was fetched). + if opts.DryRun && opts.PrintFioTable && len(txns) > 0 { printFioTable(os.Stdout, txns, syncIDs, existingIDs) }