diff options
| author | wikiapiserver | 2026-06-25 21:35:50 +0200 |
|---|---|---|
| committer | wikiapiserver | 2026-06-25 21:35:50 +0200 |
| commit | 795fb7facf403f4e3d452d2e08ba11f98e8ee997 (patch) | |
| tree | cab7c8dc6b16ef0d5cbd9871edb1bbab9fd4161c | |
| parent | 550d382014b5f3476caed167025d6c4be16d844d (diff) | |
| download | wikiapiserver-795fb7facf403f4e3d452d2e08ba11f98e8ee997.tar.gz | |
feat: log article API failures to database
- Created api_logs table (username, article_name, status_code,
response_time_ms, error, request_url)
- GetArticle logs failures (network errors and non-2xx responses)
with timing, status code, and response body
- Successful requests are not logged
| -rw-r--r-- | api/handlers.go | 28 | ||||
| -rw-r--r-- | db/db.go | 24 |
2 files changed, 50 insertions, 2 deletions
diff --git a/api/handlers.go b/api/handlers.go index c9c3031..04fcc23 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -227,16 +227,30 @@ func (h *Handler) GetArticle(w http.ResponseWriter, r *http.Request) { return } - base := "https://api.enterprise.wikimedia.com/v2/structured-contents/" + url.QueryEscape(article) - req, err := http.NewRequestWithContext(ctx, "GET", base+"?limit=1&filters[project]=en.wikipedia.org", nil) + baseURL := "https://api.enterprise.wikimedia.com/v2/structured-contents/" + url.QueryEscape(article) + queryURL := baseURL + "?limit=1&filters[project]=en.wikipedia.org" + + req, err := http.NewRequestWithContext(ctx, "GET", queryURL, nil) if err != nil { serverError(w, "could not build request") return } req.Header.Set("Authorization", "Bearer "+acct.AccessToken) + start := time.Now() resp, err := http.DefaultClient.Do(req) + elapsed := int(time.Since(start).Milliseconds()) + if err != nil { + log.Printf("article request failed: user=%s article=%s error=%v (%dms)", username, article, err, elapsed) + h.db.LogApiCall(ctx, &db.ApiLogEntry{ + Username: username, + ArticleName: article, + StatusCode: 0, + ResponseMs: elapsed, + Error: err.Error(), + RequestURL: queryURL, + }) serverError(w, "wikimedia api error") return } @@ -244,6 +258,16 @@ func (h *Handler) GetArticle(w http.ResponseWriter, r *http.Request) { if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) + log.Printf("article request failed: user=%s article=%s status=%d body=%s (%dms)", + username, article, resp.StatusCode, string(b), elapsed) + h.db.LogApiCall(ctx, &db.ApiLogEntry{ + Username: username, + ArticleName: article, + StatusCode: resp.StatusCode, + ResponseMs: elapsed, + Error: string(b), + RequestURL: queryURL, + }) http.Error(w, string(b), resp.StatusCode) return } @@ -325,3 +325,27 @@ func (d *DB) HealthCheck(ctx context.Context) error { _, err := d.conn.ExecContext(ctx, "SELECT 1") return err } + +// ApiLogEntry represents an API call log. +type ApiLogEntry struct { + Username string + ArticleName string + StatusCode int + ResponseMs int + Error string + RequestURL string +} + +// LogApiCall stores an API call log in the database. +func (d *DB) LogApiCall(ctx context.Context, entry *ApiLogEntry) error { + _, err := d.conn.ExecContext(ctx, + `INSERT INTO api_logs (username, article_name, status_code, response_time_ms, error, request_url) + VALUES (?, ?, ?, ?, ?, ?)`, + entry.Username, entry.ArticleName, entry.StatusCode, + entry.ResponseMs, entry.Error, entry.RequestURL, + ) + if err != nil { + return fmt.Errorf("log api call: %w", err) + } + return nil +} |
