{"record":{"id":"b01b67b115b823de","repo":"crowdsecurity/crowdsec","slug":"getting-unsent-usage-metrics-w","errorCode":null,"errorMessage":"getting unsent usage metrics: %w","messagePattern":"getting unsent usage metrics: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/database/metrics.go","lineNumber":90,"sourceCode":"\t}\n\n\treturn nil\n}\n\n// GetUnsentMetrics returns metrics not pushed to CAPI yet, across all sources, ordered by id.\n// Callers walk the backlog by passing back the id of the last row they consumed.\nfunc (c *Client) GetUnsentMetrics(ctx context.Context, afterID int, limit int) ([]*ent.Metric, error) {\n\tmetrics, err := c.Ent.Metric.Query().\n\t\tWhere(\n\t\t\tmetric.PushedAtIsNil(),\n\t\t\tmetric.IDGT(afterID),\n\t\t).\n\t\tOrder(ent.Asc(metric.FieldID)).\n\t\tLimit(limit).\n\t\tAll(ctx)\n\tif err != nil {\n\t\tc.Log.Warningf(\"GetUnsentMetrics: %s\", err)\n\t\treturn nil, fmt.Errorf(\"getting unsent usage metrics: %w\", err)\n\t}\n\n\treturn metrics, nil\n}\n\n// MarkStaleUsageMetricsAsSent gives up on metrics too old to be worth pushing, so a CAPI outage\n// cannot leave a growing backlog behind. The rows live on until the age flush, for cscli.\nfunc (c *Client) MarkStaleUsageMetricsAsSent(ctx context.Context, before time.Time) (int, error) {\n\tupdated, err := c.Ent.Metric.Update().\n\t\tWhere(\n\t\t\tmetric.PushedAtIsNil(),\n\t\t\tmetric.ReceivedAtLT(before),\n\t\t).\n\t\tSetPushedAt(time.Now().UTC()).\n\t\tSave(ctx)\n\tif err != nil {\n\t\tc.Log.Warningf(\"MarkStaleUsageMetricsAsSent: %s\", err)\n\t\treturn 0, fmt.Errorf(\"marking stale usage metrics as sent: %w\", err)","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/database/metrics.go#L72-L108","documentation":"GetUnsentMetrics returns metric rows not yet pushed to CAPI, ordered by ID with a limit. This error wraps a failing SELECT; having zero unsent rows is not an error — it returns an empty slice with nil error.","triggerScenarios":"Calling GetUnsentMetrics (usage-metrics push loop or cscli usage listing) when the query fails: DB file locked/unreadable, corrupted SQLite DB, disk I/O error, canceled context.","commonSituations":"Concurrent crowdsec/cscli access causing SQLite lock timeouts; corrupted DB after unclean shutdown; permission problems after moving the data directory.","solutions":["Inspect the Warningf log 'GetUnsentMetrics: <err>' for the underlying error.","Fix DB permissions/ownership or relocate off network filesystems.","Integrity-check and restore the DB if corrupt.","Retry — the push loop invokes this periodically, so transient failures recover automatically."],"exampleFix":"// before\nmetrics, err := c.GetUnsentMetrics(ctx, 100)\nif err != nil { return err }\n// after: empty is normal\nif err != nil { return fmt.Errorf(\"unsent metrics: %w\", err) }\nif len(metrics) == 0 { return nil }","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"metrics, err := client.GetUnsentMetrics(ctx, limit)\nif err != nil {\n    log.Warnf(\"unsent metrics query failed, retrying next tick: %v\", err)\n    return nil\n}\nif len(metrics) == 0 { return nil }","preventionTips":["Run the fetch periodically and retry transient failures.","Distinguish empty result from error.","Fix permissions after moving/restoring the data directory.","Keep the DB off network filesystems to avoid lock errors."],"tags":["database","sqlite","query-failure","metrics"],"backgroundTag":"database-query-failed","analyzedSha":"909b5157986a2b2c2163300fdaef5ed01289f7d2","analyzedAt":"2026-09-06T12:27:26.012Z","contentChangedAt":"2026-09-06T12:27:26.012Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}