Igor Sirotin 8125cd0262
refactor(metrics): give every metric a logos_delivery_ prefix (#4074)
* refactor(metrics): prefix node metrics with logos_delivery_

Every metric the node exports now starts with logos_delivery_. There was no
prefix mechanism before: nim-metrics derives the exported name from the Nim
identifier, no declaration passed an explicit `name = "..."`, and the waku_
convention was maintained by hand -- 69 of the 80 node metrics followed it and
11 did not (query_count, query_time_secs, event_loop_load,
event_loop_accumulated_lag_secs, postgres_payload_size_bytes, reconciliation_*,
total_* and the camelCase rendezvousPeerFoundTotal).

Identifiers are renamed rather than given a `name = "..."` argument, keeping the
invariant that the Nim identifier is the exported name and letting the compiler
check every call site.

rendezvousPeerFoundTotal becomes logos_delivery_rendezvous_peer_found: it was the
only camelCase metric, and the trailing Total was redundant since nim-metrics
already appends _total to counters at exposition time.

library/ is untouched on purpose -- `proc waku_version()` in
kernel_api/debug_node_api.nim is the exported libwaku C ABI symbol, not the gauge
of the same name in node_telemetry.nim.

BREAKING CHANGE: metric names change. Dashboards, alert rules and recording rules
that reference waku_* must be updated; see docs/operators/how-to/monitor.md.

* refactor(metrics): prefix auxiliary app metrics with logos_delivery_

Applies the same prefix to the tools shipped from this repo: liteprotocoltester
(lpt_*), networkmonitor (networkmonitor_*), chat2bridge (chat2_*) and the
lightpush_mix example (lp_mix_*).

These tools are not the delivery node and already had their own consistent
prefixes, so this commit is separable from the node rename if the intent was to
namespace only the node itself.

* chore(metrics): query old and new metric names in Grafana dashboards

212 expressions across 9 dashboards now match both the waku_* and the
logos_delivery_* spelling, so panels keep working across the upgrade and over
historical data:

  sum by (type)((increase(waku_node_errors_total{...}[$__rate_interval])
              or increase(logos_delivery_node_errors_total{...}[$__rate_interval])))

The `or` is placed around the leaf, inside every aggregation. That depth is
load-bearing: `or` keeps its right operand only for label sets absent from the
left, so `sum by (type)(old) or sum by (type)(new)` aggregates each half of the
fleet separately and then discards the right one entirely -- silently dropping
every already-upgraded node. 36 panels here collapse `instance`.

Measured against a local Prometheus scraping two targets, one exporting old names
at 10/s and one exporting new names at 20/s (truth 30/s): union outside the
aggregation gives 10, union around the leaf gives 30.

Where the leaf sits in a range vector the whole call is duplicated, since
`(a or b)[5m]` is not valid PromQL.

Once every scraped node runs a release with the new names and the old samples
have aged out of retention, the `or` half can be deleted.

* test(e2e): expect logos_delivery_-prefixed metric names

The e2e suite asserts against a live /metrics endpoint, which serves only the new
names, so these are replaced rather than unioned. libp2p_* entries are unchanged.

* docs(operators): document the logos_delivery_ metric prefix

Records that every metric the node exports is prefixed, that dependency metrics
(libp2p_*, nim_gc_*, process_*) keep their own names, and shows where the `or`
has to sit if operators maintain their own dashboards or alert rules.

* refactor(metrics): name the store fleet metrics after store, not relay

logos_delivery_relay_fleet_store_msg_size_bytes and _msg_count are declared in
waku_store/protocol_metrics.nim and recorded by the store client, but carried a
relay prefix. Renamed to logos_delivery_store_fleet_msg_size_bytes and
logos_delivery_store_fleet_msg_count.

The dashboard keeps matching the old exported name, which was
waku_relay_fleet_store_*.

Note that both metrics are wrong independently of their name, see the PR
description.
2026-07-29 18:56:50 +01:00

330 lines
10 KiB
Nim

import
std/[times, strutils, os, sets, strformat, tables],
results,
chronos,
chronos/threadsync,
metrics,
chronicles
import ./query_metrics
include db_connector/db_postgres
type DataProc* = proc(result: ptr PGresult) {.closure, gcsafe, raises: [].}
type DbConnWrapper* = ref object
dbConn: DbConn
open: bool
preparedStmts: HashSet[string] ## [stmtName's]
futBecomeFree*: Future[void]
## to notify the pgasyncpool that this conn is free, i.e. not busy
## Connection management
proc containsPreparedStmt*(dbConnWrapper: DbConnWrapper, preparedStmt: string): bool =
return dbConnWrapper.preparedStmts.contains(preparedStmt)
proc inclPreparedStmt*(dbConnWrapper: DbConnWrapper, preparedStmt: string) =
dbConnWrapper.preparedStmts.incl(preparedStmt)
proc getDbConn*(dbConnWrapper: DbConnWrapper): DbConn =
return dbConnWrapper.dbConn
proc isPgDbConnBusy*(dbConnWrapper: DbConnWrapper): bool =
if isNil(dbConnWrapper.futBecomeFree):
return false
return not dbConnWrapper.futBecomeFree.finished()
proc isPgDbConnOpen*(dbConnWrapper: DbConnWrapper): bool =
return dbConnWrapper.open
proc setPgDbConnOpen*(dbConnWrapper: DbConnWrapper, newOpenState: bool) =
dbConnWrapper.open = newOpenState
proc check(db: DbConn): Result[void, string] =
var message: string
try:
message = $db.pqErrorMessage()
except ValueError, DbError:
return err("exception in check: " & getCurrentExceptionMsg())
if message.len > 0:
let truncatedErr = message[0 ..< min(80, message.len)]
error "postgres check issue. see truncated db error.", error = truncatedErr
return err(truncatedErr)
return ok()
proc openDbConn(connString: string): Result[DbConn, string] =
## Opens a new connection.
var conn: DbConn = nil
try:
conn = open("", "", "", connString) ## included from db_postgres module
except DbError:
return err("exception opening new connection: " & getCurrentExceptionMsg())
if conn.status != CONNECTION_OK:
conn.check().isOkOr:
return err("failed to connect to database: " & error)
return err("unknown reason")
## registering the socket fd in chronos for better wait for data
let asyncFd = cast[asyncengine.AsyncFD](pqsocket(conn))
asyncengine.register(asyncFd)
return ok(conn)
proc new*(T: type DbConnWrapper, connString: string): Result[T, string] =
let dbConn = openDbConn(connString).valueOr:
return err("failed to establish a new connection: " & $error)
return ok(DbConnWrapper(dbConn: dbConn, open: true))
proc closeDbConn*(
dbConnWrapper: DbConnWrapper
): Result[void, string] {.raises: [OSError].} =
let fd = dbConnWrapper.dbConn.pqsocket()
if fd == -1:
return err("error file descriptor -1 in closeDbConn")
asyncengine.unregister(cast[asyncengine.AsyncFD](fd))
dbConnWrapper.dbConn.close()
return ok()
proc `$`(self: SqlQuery): string =
return cast[string](self)
proc sendQuery(
dbConnWrapper: DbConnWrapper, query: SqlQuery, args: seq[string]
): Future[Result[void, string]] {.async.} =
## This proc can be used directly for queries that don't retrieve values back.
if dbConnWrapper.dbConn.status != CONNECTION_OK:
dbConnWrapper.dbConn.check().isOkOr:
return err("failed to connect to database: " & $error)
return err("unknown reason")
var wellFormedQuery = ""
try:
wellFormedQuery = dbFormat(query, args)
except DbError:
return err("exception formatting the query: " & getCurrentExceptionMsg())
let success = dbConnWrapper.dbConn.pqsendQuery(cstring(wellFormedQuery))
if success != 1:
dbConnWrapper.dbConn.check().isOkOr:
return err("failed pqsendQuery: " & $error)
return err("failed pqsendQuery: unknown reason")
return ok()
proc sendQueryPrepared(
dbConnWrapper: DbConnWrapper,
stmtName: string,
paramValues: openArray[string],
paramLengths: openArray[int32],
paramFormats: openArray[int32],
): Result[void, string] {.raises: [].} =
## This proc can be used directly for queries that don't retrieve values back.
if paramValues.len != paramLengths.len or paramValues.len != paramFormats.len or
paramLengths.len != paramFormats.len:
let lengthsErrMsg =
$paramValues.len & " " & $paramLengths.len & " " & $paramFormats.len
return err("lengths discrepancies in sendQueryPrepared: " & $lengthsErrMsg)
if dbConnWrapper.dbConn.status != CONNECTION_OK:
dbConnWrapper.dbConn.check().isOkOr:
return err("failed to connect to database: " & $error)
return err("unknown reason")
var cstrArrayParams = allocCStringArray(paramValues)
defer:
deallocCStringArray(cstrArrayParams)
let nParams = cast[int32](paramValues.len)
const ResultFormat = 0 ## 0 for text format, 1 for binary format.
let success = dbConnWrapper.dbConn.pqsendQueryPrepared(
stmtName,
nParams,
cstrArrayParams,
unsafeAddr paramLengths[0],
unsafeAddr paramFormats[0],
ResultFormat,
)
if success != 1:
dbConnWrapper.dbConn.check().isOkOr:
return err("failed pqsendQueryPrepared: " & $error)
return err("failed pqsendQueryPrepared: unknown reason")
return ok()
proc waitQueryToFinish(
dbConnWrapper: DbConnWrapper, rowCallback: DataProc = nil
): Future[Result[void, string]] {.async.} =
## The 'rowCallback' param is != nil when the underlying query wants to retrieve results (SELECT.)
## For other queries, like "INSERT", 'rowCallback' should be nil.
let futDataAvailable = newFuture[void]("futDataAvailable")
proc onDataAvailable(udata: pointer) {.gcsafe, raises: [].} =
if not futDataAvailable.completed():
futDataAvailable.complete()
let asyncFd = cast[asyncengine.AsyncFD](pqsocket(dbConnWrapper.dbConn))
when not defined(windows):
asyncengine.addReader2(asyncFd, onDataAvailable).isOkOr:
dbConnWrapper.futBecomeFree.fail(newException(ValueError, $error))
return err("failed to add event reader in waitQueryToFinish: " & $error)
defer:
asyncengine.removeReader2(asyncFd).isOkOr:
return err("failed to remove event reader in waitQueryToFinish: " & $error)
else:
return err("Postgres not supported on Windows")
await futDataAvailable
## Now retrieve the result from the database
while true:
let pqResult = dbConnWrapper.dbConn.pqgetResult()
if pqResult == nil:
dbConnWrapper.dbConn.check().isOkOr:
if not dbConnWrapper.futBecomeFree.failed():
dbConnWrapper.futBecomeFree.fail(newException(ValueError, $error))
return err("error in query: " & $error)
dbConnWrapper.futBecomeFree.complete()
return ok() # reached the end of the results. The query is completed
if not rowCallback.isNil():
rowCallback(pqResult)
pqclear(pqResult)
proc containsRiskyPatterns(input: string): bool =
let riskyPatterns = @[
" OR ", " AND ", " UNION ", " SELECT ", "INSERT ", "DELETE ", "UPDATE ", "DROP ",
"EXEC ", "--", "/*", "*/",
]
for pattern in riskyPatterns:
if pattern.toLowerAscii() in input.toLowerAscii():
return true
return false
proc isSecureString(input: string): bool =
## Returns `false` if the string contains risky characters or patterns, `true` otherwise.
let riskyChars = {'\'', '\"', ';', '#', '\\', '%', '_', '/', '*', '\0'}
for ch in input:
if ch in riskyChars:
return false
if containsRiskyPatterns(input):
return false
return true
proc convertQueryToMetricLabel*(query: string): string =
## Simple query categorization. The output label is the one that should be used in query metrics
for snippetQuery, metric in QueriesToMetricMap.pairs():
if $snippetQuery in query:
return $metric
return "unknown_query_metric"
proc dbConnQuery*(
dbConnWrapper: DbConnWrapper,
query: SqlQuery,
args: seq[string],
rowCallback: DataProc,
requestId: string,
): Future[Result[void, string]] {.async, gcsafe.} =
if not requestId.isSecureString():
return err("the passed request id is not secure: " & requestId)
dbConnWrapper.futBecomeFree = newFuture[void]("dbConnQuery")
let metricLabel = convertQueryToMetricLabel($query)
var queryStartTime = getTime().toUnixFloat()
let reqIdAndQuery = "/* requestId=" & requestId & " */ " & $query
(await dbConnWrapper.sendQuery(SqlQuery(reqIdAndQuery), args)).isOkOr:
error "error in dbConnQuery", error = $error
dbConnWrapper.futBecomeFree.fail(newException(ValueError, $error))
return err("error in dbConnQuery calling sendQuery: " & $error)
let sendDuration = getTime().toUnixFloat() - queryStartTime
logos_delivery_query_time_secs.set(sendDuration, [metricLabel, "sendToDBQuery"])
queryStartTime = getTime().toUnixFloat()
(await dbConnWrapper.waitQueryToFinish(rowCallback)).isOkOr:
return err("error in dbConnQuery calling waitQueryToFinish: " & $error)
let waitDuration = getTime().toUnixFloat() - queryStartTime
logos_delivery_query_time_secs.set(waitDuration, [metricLabel, "waitFinish"])
logos_delivery_query_count.inc(labelValues = [metricLabel])
if "insert" notin ($query).toLower():
info "dbConnQuery",
requestId,
query = $query,
args,
metricLabel,
waitDbQueryDurationSecs = waitDuration,
sendToDBDurationSecs = sendDuration
return ok()
proc dbConnQueryPrepared*(
dbConnWrapper: DbConnWrapper,
stmtName: string,
paramValues: seq[string],
paramLengths: seq[int32],
paramFormats: seq[int32],
rowCallback: DataProc,
requestId: string,
): Future[Result[void, string]] {.async, gcsafe.} =
dbConnWrapper.futBecomeFree = newFuture[void]("dbConnQueryPrepared")
var queryStartTime = getTime().toUnixFloat()
dbConnWrapper.sendQueryPrepared(stmtName, paramValues, paramLengths, paramFormats).isOkOr:
dbConnWrapper.futBecomeFree.fail(newException(ValueError, $error))
error "error in dbConnQueryPrepared", error = $error
return err("error in dbConnQueryPrepared calling sendQuery: " & $error)
let sendDuration = getTime().toUnixFloat() - queryStartTime
logos_delivery_query_time_secs.set(sendDuration, [stmtName, "sendToDBQuery"])
queryStartTime = getTime().toUnixFloat()
(await dbConnWrapper.waitQueryToFinish(rowCallback)).isOkOr:
return err("error in dbConnQueryPrepared calling waitQueryToFinish: " & $error)
let waitDuration = getTime().toUnixFloat() - queryStartTime
logos_delivery_query_time_secs.set(waitDuration, [stmtName, "waitFinish"])
logos_delivery_query_count.inc(labelValues = [stmtName])
if "insert" notin stmtName.toLower():
info "dbConnQueryPrepared",
requestId,
stmtName,
paramValues,
waitDbQueryDurationSecs = waitDuration,
sendToDBDurationSecs = sendDuration
return ok()