ingotctl, HTTP layer, and self-instrumentation

Add cmd/ingotctl with four subcommands: blocks (list with stats),
inspect (series/postings dump), chunks (decode raw samples by ref), and
fsck (CRC and index integrity validation across all blocks).

Add cmd/ingothttp with JSON query endpoints: /api/v1/query_range
(Prometheus-style matrix response), /api/v1/read (matcher-based read
requests), and /epi/v1/status (DB stats snapshot). Uses JSON instead of
protobuf to maintain zero dependencies.

Add self-instrumentation via the normal Appender path so metrics are
queryable with the same API: ingot_head_series,
ingot_head_chunks_active, ingot_blocks_total, ingot_compactions_total,
ingot_wal_fsync_duration_seconds.

Supporting changes: block.Validate() and block.ReadMeta() exports,
Head.Stats() for series/chunk counts, WAL.LastSyncDuration() with timed
fsync tracking, DB.Stats() for the HTTP status endpoint.
This commit is contained in:
2026-07-04 17:40:50 -04:00
parent 0356f2e082
commit 30a93a868e
12 changed files with 1878 additions and 2 deletions
+68
View File
@@ -0,0 +1,68 @@
package ingot
import (
"git.dvdt.dev/david/ingot/labels"
)
// Self-instrumentation metric names.
const (
MetricHeadSeries = "ingot_head_series"
MetricHeadChunksActive = "ingot_head_chunks_active"
MetricBlocksTotal = "ingot_blocks_total"
MetricCompactionsTotal = "ingot_compactions_total"
MetricWALFsyncDurationS = "ingot_wal_fsync_duration_seconds"
)
// metricsRefs caches series refs for self-instrumentation metrics.
type metricsRefs struct {
headSeries uint64
headChunksActive uint64
blocksTotal uint64
compactionsTotal uint64
walFsyncDuration uint64
}
// collectMetrics snapshots the DB's internal stats and writes them as
// ingot series via the normal Appender path. Called periodically from
// the compact loop.
func (db *DB) collectMetrics() {
now := db.opts.clock()()
hs := db.head.Stats()
db.mu.RLock()
numBlocks := len(db.blocks)
db.mu.RUnlock()
compactions := db.compactionCount.Load()
walFsync := db.head.WALSyncDuration()
type metric struct {
name string
value float64
ref *uint64
}
metrics := []metric{
{MetricHeadSeries, float64(hs.NumSeries), &db.metricsR.headSeries},
{MetricHeadChunksActive, float64(hs.NumActiveChunks), &db.metricsR.headChunksActive},
{MetricBlocksTotal, float64(numBlocks), &db.metricsR.blocksTotal},
{MetricCompactionsTotal, float64(compactions), &db.metricsR.compactionsTotal},
{MetricWALFsyncDurationS, walFsync, &db.metricsR.walFsyncDuration},
}
app := db.Appender()
for _, m := range metrics {
ref := *m.ref
var ls []labels.Label
if ref == 0 {
ls = labels.FromStrings("__name__", m.name)
}
newRef, err := app.Append(ref, ls, now, m.value)
if err != nil {
// OOO rejection or other transient error — skip this cycle.
app.Rollback()
return
}
*m.ref = newRef
}
app.Commit()
}