feat(go/fio): debug logging via slog at LOG_LEVEL=DEBUG
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 13:59:22 +02:00
parent a7cf45fc95
commit f87adeff9f
5 changed files with 37 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ import (
"fuj-management/go/internal/services/banksync" "fuj-management/go/internal/services/banksync"
"fuj-management/go/internal/services/membership" "fuj-management/go/internal/services/membership"
"fuj-management/go/internal/web" "fuj-management/go/internal/web"
"log/slog"
"os" "os"
"time" "time"
) )
@@ -28,6 +29,9 @@ func main() {
os.Exit(2) 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:] cmd, args := os.Args[1], os.Args[2:]
switch cmd { switch cmd {

View File

@@ -5,7 +5,9 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/http" "net/http"
"strings"
"time" "time"
) )
@@ -25,6 +27,9 @@ func (c *apiClient) FetchTransactions(ctx context.Context, from, to time.Time) (
const layout = "2006-01-02" const layout = "2006-01-02"
url := fmt.Sprintf("https://fioapi.fio.cz/v1/rest/periods/%s/%s/%s/transactions.json", url := fmt.Sprintf("https://fioapi.fio.cz/v1/rest/periods/%s/%s/%s/transactions.json",
c.token, from.Format(layout), to.Format(layout)) 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) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
@@ -35,6 +40,7 @@ func (c *apiClient) FetchTransactions(ctx context.Context, from, to time.Time) (
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
slog.Debug("fio api: response", "status", resp.StatusCode)
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fio api: HTTP %d", resp.StatusCode) 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 { if err != nil {
return nil, err 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. // fioAPIResponse is the top-level envelope from the Fio JSON API.

View File

@@ -4,6 +4,7 @@ package fio
import ( import (
"context" "context"
"log/slog"
"net/http" "net/http"
"time" "time"
) )
@@ -36,7 +37,9 @@ func New(token, accountNum string, hc httpDoer) Client {
hc = http.DefaultClient hc = http.DefaultClient
} }
if token != "" { if token != "" {
slog.Debug("fio: client selected", "type", "api")
return &apiClient{token: token, hc: hc} return &apiClient{token: token, hc: hc}
} }
slog.Debug("fio: client selected", "type", "transparent", "account_num", accountNum)
return &transparentClient{accountNum: accountNum, hc: hc} return &transparentClient{accountNum: accountNum, hc: hc}
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/http" "net/http"
"regexp" "regexp"
"strings" "strings"
@@ -28,6 +29,10 @@ func (c *transparentClient) FetchTransactions(ctx context.Context, from, to time
from.Format("2.1.2006"), from.Format("2.1.2006"),
to.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) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -37,6 +42,7 @@ func (c *transparentClient) FetchTransactions(ctx context.Context, from, to time
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
slog.Debug("fio transparent: response", "status", resp.StatusCode)
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fio transparent: HTTP %d", resp.StatusCode) 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 { if err != nil {
return nil, err return nil, err
} }
slog.Debug("fio transparent: body read", "body_bytes", len(body))
return parseTransparentHTML(body) return parseTransparentHTML(body)
} }
@@ -63,6 +70,7 @@ func parseTransparentHTML(body []byte) ([]Transaction, error) {
rows := extractSecondTableRows(body) rows := extractSecondTableRows(body)
var txns []Transaction var txns []Transaction
var droppedBadDate, droppedNonpositive int
for _, row := range rows { for _, row := range rows {
col := func(i int) string { col := func(i int) string {
if i < len(row) { if i < len(row) {
@@ -72,7 +80,12 @@ func parseTransparentHTML(body []byte) ([]Transaction, error) {
} }
dateStr := parseCzechDate(col(tColDate)) dateStr := parseCzechDate(col(tColDate))
amount := parseCzechAmount(col(tColAmount)) amount := parseCzechAmount(col(tColAmount))
if dateStr == "" || amount <= 0 { if dateStr == "" {
droppedBadDate++
continue
}
if amount <= 0 {
droppedNonpositive++
continue continue
} }
txns = append(txns, Transaction{ txns = append(txns, Transaction{
@@ -86,6 +99,11 @@ func parseTransparentHTML(body []byte) ([]Transaction, error) {
BankID: "", // not available on HTML path 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 return txns, nil
} }

View File

@@ -112,8 +112,8 @@ func SyncToSheets(
}) })
} }
// 4b. Optional debug table (dry-run only). // 4b. Optional debug table (dry-run only; suppress when nothing was fetched).
if opts.DryRun && opts.PrintFioTable { if opts.DryRun && opts.PrintFioTable && len(txns) > 0 {
printFioTable(os.Stdout, txns, syncIDs, existingIDs) printFioTable(os.Stdout, txns, syncIDs, existingIDs)
} }