diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 309cf55b1..cfad07e13 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -343,6 +343,9 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive + # Full history + tags so `git describe --tags` can version the image + # (bare tag on release commits, tag-distance-sha on snapshots). + fetch-depth: 0 - name: Download bookworm moxygen tarball env: @@ -380,11 +383,22 @@ jobs: echo "short=$SHORT" >> "$GITHUB_OUTPUT" echo "rolling=${LABEL}-latest" >> "$GITHUB_OUTPUT" + - name: Resolve build version + id: version + run: | + # Baked into the binary and emitted as an OCI label. Matching only + # v-prefixed tags is required: the repo carries moving tags that are + # not versions, and an unfiltered describe returns one of those. + V="$(git describe --tags --match 'v[0-9]*' --always)" + echo "version=$V" >> "$GITHUB_OUTPUT" + echo "==> build version: $V" + - name: Build Docker image run: | IMAGE="ghcr.io/${{ github.repository }}" ARCH="${{ matrix.arch }}" docker build -f docker/Dockerfile \ + --build-arg MOQX_VERSION_STRING="${{ steps.version.outputs.version }}" \ -t "${IMAGE}:${{ steps.tags.outputs.short }}-${ARCH}" \ -t "${IMAGE}:${{ steps.tags.outputs.rolling }}-${ARCH}" \ . diff --git a/.github/workflows/version-release.yml b/.github/workflows/version-release.yml index f7cc852db..0aadec0cf 100644 --- a/.github/workflows/version-release.yml +++ b/.github/workflows/version-release.yml @@ -324,8 +324,11 @@ jobs: # path. Combined with the bundle step below this produces a # self-contained tarball that runs on noble / RHEL 9 / AL2023 # without needing distro-specific packages installed. + # Pass the tag explicitly: the checkout is shallow and the tag may + # not exist yet, so git describe would yield a bare sha. cmake -S . -B _build -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ + -DMOQX_VERSION_STRING="${{ needs.validate.outputs.tag }}" \ -DCMAKE_PREFIX_PATH="$(cat .scratch/cmake_prefix_path.txt)" \ -DCMAKE_MODULE_PATH="${GITHUB_WORKSPACE}/cmake;${GITHUB_WORKSPACE}/deps/moxygen/build/fbcode_builder/CMake" \ -DCMAKE_C_FLAGS="${{ matrix.cxx_flags }}" \ diff --git a/.gitignore b/.gitignore index 4a25ebaac..c13cb2ac3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Build outputs /build/ /build-*/ +/_build/ /cmake-build-*/ /CMakeFiles/ CMakeCache.txt @@ -39,3 +40,6 @@ docker/cloudflare.ini # gh-pages dashboard (lives on gh-pages branch, not main) /gh-pages/ docker/grafana/provisioning/dashboards/archive/ + +# Version stamp for source trees with no .git (cmake/MoqxVersion.cmake). +/VERSION diff --git a/CMakeLists.txt b/CMakeLists.txt index 1039741fd..7f3d200a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,9 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "" FORCE) endif() +# Defines MOQX_VERSION_STRING and the moqx_version interface target. +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/MoqxVersion.cmake) + option(MOQX_BUILD_TESTS "Build tests" ON) option(MOQX_BUILD_BENCHMARKS "Build benchmarks" OFF) option(MOQX_ENABLE_SANITIZERS "Enable ASAN/UBSAN (non-Release)" OFF) @@ -202,7 +205,8 @@ target_link_libraries(moqx_core PUBLIC ) target_compile_options(moqx_core PRIVATE -Wall -Wextra -Wpedantic) -target_compile_definitions(moqx_core PRIVATE MOQX_VERSION="${PROJECT_VERSION}") +# PUBLIC so dependents can include moqx/Version.h. +target_link_libraries(moqx_core PUBLIC moqx_version) # --- Config types (resolved Config structs, header-only, no rfl dependency) --- # Library consumers that only need the Config API link this. @@ -319,3 +323,6 @@ endif() include(${PROJECT_SOURCE_DIR}/cmake/Lint.cmake) install(TARGETS moqx moqx-issuer moqx_core moqx_config_loader) + +# Identifies an unpacked tarball without running the binary. +install(FILES ${CMAKE_BINARY_DIR}/VERSION DESTINATION .) diff --git a/cmake/MoqxVersion.cmake b/cmake/MoqxVersion.cmake new file mode 100644 index 000000000..327f972f2 --- /dev/null +++ b/cmake/MoqxVersion.cmake @@ -0,0 +1,95 @@ +# Build identifier compiled into the binary (/info, --version, log banner) and +# written to ${CMAKE_BINARY_DIR}/VERSION for packaging. +# +# Format is git describe output keeping the tag's "v" prefix, e.g. v0.2.1, +# v0.2.1-14-gabc1234, v0.2.1-14-gabc1234-dirty, or a bare sha when no v* tag is +# reachable. Resolution order: +# +# 1. -DMOQX_VERSION_STRING explicit (CI, docker --build-arg) +# 2. git describe any clone, including local dev builds +# 3. /VERSION for trees with no .git (source tarballs) +# 4. v${PROJECT_VERSION} +# +# git outranks the VERSION file because that file is gitignored: a stale one +# left in a working tree would silently pin every later build. +# +# Only v-prefixed numeric tags are matched — the repo carries moving tags that +# are not versions, and an unfiltered describe would return one of those. +# +# Resolution runs at configure time, so a dev who commits without re-running +# cmake keeps the previous string until the next configure. + +function(_moqx_version_from_git out_var) + set(${out_var} "" PARENT_SCOPE) + + find_package(Git QUIET) + if(NOT GIT_FOUND) + return() + endif() + + # A worktree's .git is a file, not a directory. + if(NOT EXISTS "${PROJECT_SOURCE_DIR}/.git") + return() + endif() + + execute_process( + COMMAND "${GIT_EXECUTABLE}" describe --tags --match "v[0-9]*" --always + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + OUTPUT_VARIABLE _described + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + RESULT_VARIABLE _rc + ) + if(NOT _rc EQUAL 0 OR NOT _described) + return() + endif() + + # describe --dirty only inspects tracked files; status also reports untracked + # ones, so a tree with new files is not mistaken for a clean commit. Ignored + # paths (build dirs, VERSION) are excluded by default. + execute_process( + COMMAND "${GIT_EXECUTABLE}" status --porcelain + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + OUTPUT_VARIABLE _status + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + if(_status) + set(_described "${_described}-dirty") + endif() + + set(${out_var} "${_described}" PARENT_SCOPE) +endfunction() + +if(MOQX_VERSION_STRING) + set(_moqx_version_source "explicit -DMOQX_VERSION_STRING") +else() + _moqx_version_from_git(MOQX_VERSION_STRING) + if(MOQX_VERSION_STRING) + set(_moqx_version_source "git describe") + elseif(EXISTS "${PROJECT_SOURCE_DIR}/VERSION") + file(READ "${PROJECT_SOURCE_DIR}/VERSION" MOQX_VERSION_STRING) + string(STRIP "${MOQX_VERSION_STRING}" MOQX_VERSION_STRING) + set(_moqx_version_source "VERSION file") + else() + set(MOQX_VERSION_STRING "v${PROJECT_VERSION}") + set(_moqx_version_source "project() fallback") + endif() +endif() + +message(STATUS "moqx version: ${MOQX_VERSION_STRING} (from ${_moqx_version_source})") + +set(MOQX_VERSION_HEADER_DIR "${CMAKE_BINARY_DIR}/generated") +configure_file( + "${PROJECT_SOURCE_DIR}/cmake/Version.h.in" + "${MOQX_VERSION_HEADER_DIR}/moqx/Version.h" + @ONLY +) + +# Interface target so any binary can include moqx/Version.h. +add_library(moqx_version INTERFACE) +target_include_directories(moqx_version INTERFACE "${MOQX_VERSION_HEADER_DIR}") + +# Shipped in the install tree: identifies an unpacked tarball without running +# the binary. Images carry OCI labels instead. +file(WRITE "${CMAKE_BINARY_DIR}/VERSION" "${MOQX_VERSION_STRING}\n") diff --git a/cmake/Version.h.in b/cmake/Version.h.in new file mode 100644 index 000000000..29bec2500 --- /dev/null +++ b/cmake/Version.h.in @@ -0,0 +1,25 @@ +/* + * Copyright (c) OpenMOQ contributors. + * This source code is licensed under the Apache 2.0 license found in the + * LICENSE file in the root directory of this source tree. + * + * GENERATED FILE — do not edit. Source template: cmake/Version.h.in, + * derivation: cmake/MoqxVersion.cmake. + */ + +#pragma once + +#include + +// Macro form: usable in string-literal concatenation (e.g. the admin /info +// JSON body). Prefer openmoq::moqx::kVersion in new code. +#define MOQX_VERSION "@MOQX_VERSION_STRING@" + +namespace openmoq::moqx { + +// Canonical build identifier: a git describe string with the "v" tag prefix +// retained (v0.2.1, v0.2.1-14-gabc1234, v0.2.1-14-gabc1234-dirty), or a bare +// commit sha when no v* tag is reachable. +inline constexpr std::string_view kVersion{MOQX_VERSION}; + +} // namespace openmoq::moqx diff --git a/docker/Dockerfile b/docker/Dockerfile index 6db4e514d..9acc4abb3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,8 +20,13 @@ COPY src/ src/ COPY deps/catapult/ deps/catapult/ COPY deps/moxygen/build/fbcode_builder/CMake deps/moxygen/build/fbcode_builder/CMake +# The build context excludes .git, so cmake cannot derive the version here; +# CI passes it in. Empty falls back to the static project version. +ARG MOQX_VERSION_STRING="" + RUN cmake -S . -B _build --preset default \ -DCMAKE_PREFIX_PATH=/opt/moxygen \ + -DMOQX_VERSION_STRING="${MOQX_VERSION_STRING}" \ -DBUILD_TESTING=OFF -DMOQX_BUILD_TESTS=OFF \ && cmake --build _build -j$(nproc) \ && cmake --install _build --prefix /install @@ -36,6 +41,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ curl gettext-base \ && rm -rf /var/lib/apt/lists/* +# Same value the binary reports, as image metadata: `docker inspect` and +# registry UIs can identify an image without pulling or running it. +# ARGs do not cross stages, so it is redeclared here. +ARG MOQX_VERSION_STRING="" +LABEL org.opencontainers.image.title="moqx" \ + org.opencontainers.image.description="MoQ relay" \ + org.opencontainers.image.version="${MOQX_VERSION_STRING}" \ + org.opencontainers.image.source="https://github.com/openmoq/moqx" \ + org.opencontainers.image.licenses="Apache-2.0" + COPY --from=builder /install/bin/moqx /usr/local/bin/moqx COPY --from=builder /install/bin/moqx-issuer /usr/local/bin/moqx-issuer COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh diff --git a/docker/grafana/provisioning/dashboards/moqx-overview.json b/docker/grafana/provisioning/dashboards/moqx-overview.json index c9436826d..1a07e4698 100644 --- a/docker/grafana/provisioning/dashboards/moqx-overview.json +++ b/docker/grafana/provisioning/dashboards/moqx-overview.json @@ -60,7 +60,7 @@ "id": 40, "options": { "afterRender": "", - "content": "
\n
\n
\n \n
moqx {{#if version}}v{{version}}{{else}}v?{{/if}}
\n
\n
\n
\n
NAME:{{nodename}} [ relay-id: {{relay_id}} ]
\n
HOSTNAME:moqx-main.ci.openmoq.org [ ipv4: 172.234.201.95 ]
\n \n
\n
\n
HW:{{machine}} · {{[Value #B]}} vCPU · {{[Value #C]}} GiB RAM · {{[Value #D]}} GiB HD
\n
CPU:{{#if model_name}}{{model_name}}{{else}}—{{/if}}
\n
OS:{{#if pretty_name}}{{pretty_name}}{{else}}—{{/if}} [ {{release}} ]
\n
DATE:{{date [Value #J] \"YYYY-MM-DD HH:mm:ss Z\"}}
\n
UPTIME:{{[Value #I]}}d {{[Value #L]}}h {{[Value #M]}}m
\n
\n
\n
\n platform detail ↗\n
", + "content": "
\n
\n
\n \n
moqx {{#if relay_version}}{{relay_version}}{{else}}\u2014{{/if}}
\n
\n
\n
\n
NAME:{{nodename}} [ relay-id: {{relay_id}} ]
\n
HOSTNAME:moqx-main.ci.openmoq.org [ ipv4: 172.234.201.95 ]
\n \n
\n
\n
HW:{{machine}} · {{[Value #B]}} vCPU · {{[Value #C]}} GiB RAM · {{[Value #D]}} GiB HD
\n
CPU:{{#if model_name}}{{model_name}}{{else}}—{{/if}}
\n
OS:{{#if pretty_name}}{{pretty_name}}{{else}}—{{/if}} [ {{release}} ]
\n
DATE:{{date [Value #J] \"YYYY-MM-DD HH:mm:ss Z\"}}
\n
UPTIME:{{[Value #I]}}d {{[Value #L]}}h {{[Value #M]}}m
\n
\n
\n
\n platform detail ↗\n
", "contentPartials": [], "defaultContent": "Host metrics not yet available (node-exporter warming up).", "editor": { @@ -74,7 +74,7 @@ "externalStyles": [], "helpers": "", "renderMode": "everyRow", - "styles": ".mx-wrap { container-type: inline-size; height: 100%; }\n.mx-banner { display: flex; align-items: center; justify-content: center; gap: 24px;\n padding: 18px 8px 4px; height: 100%; font-family: inherit; }\n.mx-logo { display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 96px; }\n.mx-cols { display: flex; align-items: center; gap: 22px; font-size: 12.5px;\n font-family: monospace; white-space: nowrap; overflow: hidden; }\n.mx-c1 { line-height: 2.15; }\n.mx-c2 { line-height: 1.8; }\n.mx-lbl { display: inline-block; font-weight: 600; margin-right: 4px; }\n.mx-urlbox { margin-top: 13px; border: 1px solid rgba(236,178,46,.35); border-radius: 6px;\n padding: 3px 8px 3px 5px; display: inline-block; line-height: 1.9; }\n/* shrink level 1: logo gone, both columns stay, no scrollbar */\n@container (max-width: 1080px) {\n .mx-logo { display: none; }\n .mx-banner { padding-top: 8px; }\n}\n/* shrink level 2: single column of LABEL: value, vertical scroll */\n@container (max-width: 660px) {\n .mx-banner { align-items: flex-start; padding-top: 4px; }\n .mx-cols { flex-direction: column; align-items: flex-start; gap: 6px;\n overflow-y: auto; overflow-x: hidden; max-height: 100%; }\n .mx-c1, .mx-c2 { line-height: 1.9; }\n .mx-urlbox { margin-top: 4px; }\n}", + "styles": ".mx-wrap { container-type: inline-size; height: 100%; }\n.mx-banner { display: flex; align-items: center; justify-content: center; gap: 24px;\n padding: 18px 8px 4px; height: 100%; font-family: inherit; }\n.mx-logo { display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 96px; }\n.mx-logo-img { height: 78px; max-width: 100%; }\n.mx-cols { display: flex; align-items: center; gap: 22px; font-size: 12.5px;\n font-family: monospace; white-space: nowrap; overflow: hidden; }\n.mx-c1 { line-height: 2.15; }\n.mx-c2 { line-height: 1.8; }\n.mx-lbl { display: inline-block; font-weight: 600; margin-right: 4px; }\n.mx-urlbox { margin-top: 13px; border: 1px solid rgba(236,178,46,.35); border-radius: 6px;\n padding: 3px 8px 3px 5px; display: inline-block; line-height: 1.9; }\n/* shrink level 1: logo + version collapse to a one-line strip above the data */\n@container (max-width: 1080px) {\n .mx-banner { flex-direction: column; align-items: flex-start;\n justify-content: flex-start; gap: 2px; padding: 2px 10px 0; }\n .mx-logo { flex-direction: row; align-items: center; gap: 10px; min-width: 0; }\n .mx-logo-img { height: 30px; }\n .mx-cols { gap: 18px; }\n .mx-c1 { line-height: 1.7; }\n .mx-c2 { line-height: 1.7; }\n .mx-urlbox { margin-top: 5px; padding-top: 1px; padding-bottom: 1px;\n line-height: 1.7; }\n}\n/* shrink level 2: single column of LABEL: value, vertical scroll */\n@container (max-width: 660px) {\n .mx-cols { flex-direction: column; align-items: flex-start; gap: 6px;\n overflow-y: auto; overflow-x: hidden; }\n .mx-c1, .mx-c2 { line-height: 1.9; }\n .mx-urlbox { margin-top: 4px; }\n}", "wrap": true }, "pluginVersion": "6.3.0", @@ -139,7 +139,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace(group by (version) (moqx_build_info), \"instance\", \"moqx-host\", \"\", \"\")", + "expr": "label_replace(label_replace(group by (version) (topk(1, timestamp(moqx_build_info))), \"relay_version\", \"$1\", \"version\", \"(.*)\"), \"instance\", \"moqx-host\", \"\", \"\")", "format": "table", "instant": true, "range": false, @@ -464,7 +464,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Known relays in the mesh (PLACEHOLDER \u2014 self only until upstream peers are exported from /state).", "fieldConfig": { "defaults": { "thresholds": { @@ -485,14 +484,14 @@ }, "gridPos": { "h": 4, - "w": 3, + "w": 2, "x": 0, "y": 6 }, "id": 112, "options": { "afterRender": "", - "content": "
\n
{{[Value #A]}}
\n
MAX: {{[Value #B]}}
\n
", + "content": "
\n
{{[Value #A]}}
\n
MAX: {{[Value #B]}}
\n
", "contentPartials": [], "defaultContent": "
", "editor": { @@ -514,7 +513,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace(count(moqx_relay_active_sessions), \"k\", \"x\", \"\", \"\")", + "expr": "label_replace(count(moqx_peer_relay) or vector(0), \"k\", \"x\", \"\", \"\")", "format": "table", "instant": true, "range": false, @@ -525,7 +524,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace(max_over_time((count(moqx_relay_active_sessions))[$__range:30s]), \"k\", \"x\", \"\", \"\")", + "expr": "label_replace(max_over_time((count(moqx_peer_relay))[$__range:5s]) or vector(0), \"k\", \"x\", \"\", \"\")", "format": "table", "instant": true, "range": false, @@ -549,7 +548,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Distinct namespaces with tracks visible in the relay /state. Peak is over the dashboard time range.", "fieldConfig": { "defaults": { "thresholds": { @@ -570,14 +568,14 @@ }, "gridPos": { "h": 4, - "w": 3, - "x": 3, + "w": 2, + "x": 2, "y": 6 }, - "id": 60, + "id": 120, "options": { "afterRender": "", - "content": "
\n
{{[Value #A]}}
\n
MAX: {{[Value #B]}}
\n
", + "content": "
\n
{{[Value #A]}}
\n
MAX: {{[Value #B]}}
\n
", "contentPartials": [], "defaultContent": "
", "editor": { @@ -610,7 +608,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace(max_over_time((count(count by (namespace) (moqx_track_subscribers)))[$__range:30s]) or vector(0), \"k\", \"x\", \"\", \"\")", + "expr": "label_replace(max_over_time((count(count by (namespace) (moqx_track_subscribers)))[$__range:5s]) or vector(0), \"k\", \"x\", \"\", \"\")", "format": "table", "instant": true, "range": false, @@ -634,7 +632,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Distinct (namespace, track) pairs visible in the relay /state. Peak is over the dashboard time range.", "fieldConfig": { "defaults": { "thresholds": { @@ -655,14 +652,14 @@ }, "gridPos": { "h": 4, - "w": 3, - "x": 6, + "w": 2, + "x": 4, "y": 6 }, "id": 61, "options": { "afterRender": "", - "content": "
\n
{{[Value #A]}}
\n
MAX: {{[Value #B]}}
\n
", + "content": "
\n
{{[Value #A]}}
\n
MAX: {{[Value #B]}}
\n
", "contentPartials": [], "defaultContent": "
", "editor": { @@ -695,7 +692,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace(max_over_time((count(count by (namespace, track) (moqx_track_subscribers)))[$__range:30s]) or vector(0), \"k\", \"x\", \"\", \"\")", + "expr": "label_replace(max_over_time((count(count by (namespace, track) (moqx_track_subscribers)))[$__range:5s]) or vector(0), \"k\", \"x\", \"\", \"\")", "format": "table", "instant": true, "range": false, @@ -719,7 +716,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Active publishers connected to this relay. Peak is over the dashboard time range.", "fieldConfig": { "defaults": { "thresholds": { @@ -740,14 +736,14 @@ }, "gridPos": { "h": 4, - "w": 3, - "x": 9, + "w": 2, + "x": 6, "y": 6 }, - "id": 113, + "id": 118, "options": { "afterRender": "", - "content": "
\n
{{[Value #A]}}
\n
MAX: {{[Value #B]}}
\n
", + "content": "
\n
{{[Value #A]}}
\n
MAX: {{[Value #B]}}
\n
", "contentPartials": [], "defaultContent": "
", "editor": { @@ -780,14 +776,14 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace(max_over_time((max(moqx_subActivePublishers))[$__range:30s]) or vector(0), \"k\", \"x\", \"\", \"\")", + "expr": "label_replace(max(max_over_time(moqx_subActivePublishers[$__range])) or vector(0), \"k\", \"x\", \"\", \"\")", "format": "table", "instant": true, "range": false, "refId": "B" } ], - "title": "Publishers", + "title": "Pubs", "transformations": [ { "id": "joinByField", @@ -804,7 +800,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Aggregate active subscriptions (authoritative under load). Peak is over the dashboard time range.", "fieldConfig": { "defaults": { "thresholds": { @@ -825,14 +820,14 @@ }, "gridPos": { "h": 4, - "w": 3, - "x": 12, + "w": 2, + "x": 8, "y": 6 }, - "id": 114, + "id": 113, "options": { "afterRender": "", - "content": "
\n
{{[Value #A]}}
\n
MAX: {{[Value #B]}}
\n
", + "content": "
\n
{{[Value #A]}}
\n
MAX: {{[Value #B]}}
\n
", "contentPartials": [], "defaultContent": "
", "editor": { @@ -865,14 +860,14 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace(max_over_time((max(moqx_pubActiveSubscriptions))[$__range:30s]) or vector(0), \"k\", \"x\", \"\", \"\")", + "expr": "label_replace(max(max_over_time(moqx_pubActiveSubscriptions[$__range])) or vector(0), \"k\", \"x\", \"\", \"\")", "format": "table", "instant": true, "range": false, "refId": "B" } ], - "title": "Subscribers", + "title": "Subs", "transformations": [ { "id": "joinByField", @@ -889,60 +884,38 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "p95 QUIC RTT across all connections. Rising latency + rising loss = overload. Peak over the dashboard range.", + "description": "QUIC RTT: current p50 readout, with p50 and p99 overlaid behind it.", "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, + "defaults": {}, "overrides": [] }, "gridPos": { "h": 4, "w": 3, - "x": 15, + "x": 10, "y": 6 }, "id": 108, + "maxDataPoints": 500, "options": { - "afterRender": "", - "content": "
\n
{{[Value #A]}} ms
\n
MAX: {{[Value #B]}} ms
\n
", - "contentPartials": [], - "defaultContent": "
", "editor": { "format": "auto", - "language": "markdown" + "height": 600 }, - "editorMode": "html", - "editors": [], - "externalStyles": [], - "helpers": "", - "renderMode": "everyRow", - "styles": "", - "wrap": true + "editorMode": "code", + "getOption": "\nconst frames = context.panel.data.series || [];\nconst INK = '#e6edf3', MUTED = 'rgba(230,237,243,.45)';\nconst C = { p50: '#5AA7BE', p99: '#C2562F' };\n\nfunction pairs(f) {\n const t = f.fields.find((x) => x.type === 'time');\n const v = f.fields.find((x) => x.type === 'number');\n if (!t || !v) return [];\n const tv = t.values.toArray ? t.values.toArray() : t.values;\n const vv = v.values.toArray ? v.values.toArray() : v.values;\n const out = [];\n for (let i = 0; i < tv.length; i++) {\n if (vv[i] != null && !Number.isNaN(vv[i])) out.push([tv[i], vv[i]]);\n }\n return out;\n}\nfunction label(f, i) {\n const v = f.fields.find((x) => x.type === 'number');\n const c = (v && v.config) || {};\n return c.displayNameFromDS || c.displayName || f.name || ('s' + i);\n}\n\nconst got = {};\nframes.forEach((f, i) => { got[label(f, i)] = pairs(f); });\nconst p50 = got['p50'] || [];\nconst p99 = got['p99'] || [];\nconst last = (a) => (a.length ? a[a.length - 1][1] : null);\nconst fmt = (v) => (v == null ? '\u2013' : Number(v).toFixed(1));\n\n\n\nfunction line(data, color, width, fill) {\n return {\n type: 'line', data, smooth: 0.1, showSymbol: false, silent: true,\n lineStyle: { color, width, cap: 'round' },\n areaStyle: { color, opacity: fill },\n emphasis: { disabled: true }, animation: false,\n };\n}\n\nconst LEGEND_FONT = '10px ui-monospace, monospace';\nconst key = (top, color, text) => ({\n type: 'text', right: 8, top,\n style: { text: '\\u25CF ' + text, fill: MUTED, font: LEGEND_FONT,\n rich: { d: { color, font: LEGEND_FONT } } },\n});\nconst keyDot = (top, color) => ({\n type: 'text', right: 30, top,\n style: { text: '\\u25CF', fill: color, font: LEGEND_FONT },\n});\nconst keyLabel = (top, text) => ({\n type: 'text', right: 8, top,\n style: { text, fill: MUTED, font: LEGEND_FONT },\n});\n\nreturn {\n animation: false,\n grid: { left: -2, right: -2, top: '44%', bottom: -2 },\n xAxis: { type: 'time', show: false },\n yAxis: { type: 'value', show: false, scale: true },\n series: [line(p99, C.p99, 1.2, 0.10), line(p50, C.p50, 1.8, 0.22)],\n graphic: [\n { type: 'text', left: 'center', top: 18,\n style: { text: fmt(last(p50)) + ' ms', fill: '#3987e5',\n font: '600 26px Inter, system-ui, sans-serif' } },\n keyDot(2, C.p99), keyLabel(2, 'p99'),\n keyDot(15, C.p50), keyLabel(15, 'p50'),\n ],\n};\n", + "renderer": "canvas" }, - "pluginVersion": "6.3.0", + "pluginVersion": "7.2.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace((round(histogram_quantile(0.95, sum by (le) (rate(moqx_quicRttSample_milliseconds_bucket[$__rate_interval]))), 0.1) >= 0) or vector(0), \"k\", \"x\", \"\", \"\")", - "format": "table", - "instant": true, - "range": false, + "expr": "histogram_quantile(0.50, sum by (le) (rate(moqx_quicRttSample_milliseconds_bucket[10s]))) >= 0 or vector(0)", + "interval": "3s", + "legendFormat": "p50", "refId": "A" }, { @@ -950,335 +923,167 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace(round(max_over_time(((histogram_quantile(0.95, sum by (le) (rate(moqx_quicRttSample_milliseconds_bucket[1m])))) >= 0)[$__range:1m]), 0.1) or vector(0), \"k\", \"x\", \"\", \"\")", - "format": "table", - "instant": true, - "range": false, + "expr": "histogram_quantile(0.99, sum by (le) (rate(moqx_quicRttSample_milliseconds_bucket[10s]))) >= 0 or vector(0)", + "interval": "3s", + "legendFormat": "p99", "refId": "B" } ], "title": "Latency", - "transformations": [ - { - "id": "joinByField", - "options": { - "byField": "k", - "mode": "outer" - } - } - ], - "type": "marcusolsson-dynamictext-panel" + "transparent": false, + "type": "volkovlabs-echarts-panel" }, { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "QUIC packet-loss ratio and socket-buffer datagram drops (read+write). Buffer drops are the overload signal \u2014 rising together with latency means the relay is saturating. (Garbage/undecodable packet drops from internet background noise are excluded; see the QUIC transport row.)", + "description": "Combined loss: QUIC packet loss plus socket-buffer datagram drops, over all packets handled. Blue below 0.5%, amber to 2%, red above.", "fieldConfig": { "defaults": { - "color": { - "fixedColor": "#3987e5", - "mode": "fixed" - }, "decimals": 1, "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#3987e5", - "value": 0 - } - ] - }, - "unit": "pps" + "unit": "percent" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "loss" - }, - "properties": [ - { - "id": "unit", - "value": "percent" - }, - { - "id": "decimals", - "value": 2 - }, - { - "id": "color", - "value": { - "mode": "thresholds" - } - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "#3987e5", - "value": 0 - }, - { - "color": "#c98500", - "value": 0.5 - }, - { - "color": "#e66767", - "value": 2 - } - ] - } - } - ] - } - ] + "overrides": [] }, "gridPos": { "h": 4, "w": 3, - "x": 18, + "x": 13, "y": 6 }, "id": 116, + "maxDataPoints": 500, "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "center", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false + "editor": { + "format": "auto", + "height": 600 }, - "showPercentChange": false, - "textMode": "value_and_name", - "wideLayout": true + "editorMode": "code", + "getOption": "\nconst frames = context.panel.data.series || [];\nfunction pairs(fr) {\n if (!fr) return [];\n const t = fr.fields.find((x) => x.type === 'time');\n const v = fr.fields.find((x) => x.type === 'number');\n if (!t || !v) return [];\n const tv = t.values.toArray ? t.values.toArray() : t.values;\n const vv = v.values.toArray ? v.values.toArray() : v.values;\n const out = [];\n for (let i = 0; i < tv.length; i++) {\n if (vv[i] != null && !Number.isNaN(vv[i])) out.push([tv[i], vv[i]]);\n }\n return out;\n}\nconst data = pairs(frames[0]);\nconst last = data.length ? data[data.length - 1][1] : 0;\nconst color = last >= 2 ? '#e0453c' : last >= 0.5 ? '#c98500' : '#3987e5';\nreturn {\n animation: false,\n grid: { left: -2, right: -2, top: '46%', bottom: -2 },\n xAxis: { type: 'time', show: false },\n yAxis: { type: 'value', show: false, scale: true, min: 0 },\n series: [{\n type: 'line', data, smooth: 0.1, showSymbol: false, silent: true,\n lineStyle: { color, width: 1.8, cap: 'round' },\n areaStyle: { color, opacity: 0.20 },\n emphasis: { disabled: true }, animation: false,\n }],\n graphic: [\n { type: 'text', left: 'center', top: 16,\n style: { text: last.toFixed(1) + ' %', fill: color,\n font: '600 26px Inter, system-ui, sans-serif' } },\n ],\n};\n", + "renderer": "canvas" }, - "pluginVersion": "12.4.6", + "pluginVersion": "7.2.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "expr": "100 * sum(rate(moqx_quicPacketLoss_total[$__rate_interval])) / clamp_min(sum(rate(moqx_quicPacketsSent_total[$__rate_interval])), 1)", + "expr": "100 * (sum(rate(moqx_quicPacketLoss_total[10s])) + sum(rate(moqx_quicDatagramsDroppedOnRead_total[10s])) + sum(rate(moqx_quicDatagramsDroppedOnWrite_total[10s]))) / clamp_min(sum(rate(moqx_quicPacketsSent_total[10s])) + sum(rate(moqx_quicPacketsReceived_total[10s])), 1)", + "interval": "3s", "legendFormat": "loss", "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "expr": "sum(rate(moqx_quicDatagramsDroppedOnRead_total[$__rate_interval])) + sum(rate(moqx_quicDatagramsDroppedOnWrite_total[$__rate_interval]))", - "legendFormat": "drops", - "refId": "B" } ], - "title": "Loss / drops", - "type": "stat" + "title": "Loss", + "transparent": false, + "type": "volkovlabs-echarts-panel" }, { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "Relay application throughput: QUIC payload bytes read/written (incl. MoQT framing + control \u2014 a close PROXY for object BW until the relay exports a true object-bytes-delivered counter). OS/NIC-level BW lives in the platform drill-down.", "fieldConfig": { "defaults": { - "color": { - "fixedColor": "#3987e5", - "mode": "fixed" - }, - "decimals": 1, "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#3987e5", - "value": 0 - } - ] - }, "unit": "bps" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "out" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "#d95926", - "mode": "fixed" - } - } - ] - } - ] + "overrides": [] }, "gridPos": { "h": 4, - "w": 3, - "x": 21, + "w": 4, + "x": 16, "y": 6 }, - "id": 115, - "links": [ - { - "title": "Platform detail", - "url": "/grafana/public-dashboards/449a0c28a06a4838ac3fcdccd8ef0f37" - } - ], + "id": 119, + "maxDataPoints": 500, "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false + "editor": { + "format": "auto", + "height": 600 }, - "showPercentChange": false, - "textMode": "value_and_name", - "wideLayout": true + "editorMode": "code", + "getOption": "\nconst frames = context.panel.data.series || [];\nconst f = frames[0];\nfunction pairs(fr) {\n if (!fr) return [];\n const t = fr.fields.find((x) => x.type === 'time');\n const v = fr.fields.find((x) => x.type === 'number');\n if (!t || !v) return [];\n const tv = t.values.toArray ? t.values.toArray() : t.values;\n const vv = v.values.toArray ? v.values.toArray() : v.values;\n const out = [];\n for (let i = 0; i < tv.length; i++) {\n if (vv[i] != null && !Number.isNaN(vv[i])) out.push([tv[i], vv[i]]);\n }\n return out;\n}\nconst data = pairs(f);\nconst last = data.length ? data[data.length - 1][1] : 0;\nfunction fmt(bps) {\n const u = ['b/s', 'kb/s', 'Mb/s', 'Gb/s', 'Tb/s'];\n let i = 0, v = bps;\n while (v >= 1000 && i < u.length - 1) { v /= 1000; i++; }\n return [v.toFixed(1), u[i]];\n}\nconst [num, unit] = fmt(last);\nreturn {\n animation: false,\n grid: { left: -2, right: -2, top: '46%', bottom: -2 },\n xAxis: { type: 'time', show: false },\n yAxis: { type: 'value', show: false, scale: true, min: 0 },\n series: [{\n type: 'line', data, smooth: 0.1, showSymbol: false, silent: true,\n lineStyle: { color: '#5AA7BE', width: 1.8, cap: 'round' },\n areaStyle: { color: '#5AA7BE', opacity: 0.20 },\n emphasis: { disabled: true }, animation: false,\n }],\n graphic: [\n { type: 'text', left: 'center', top: 16,\n style: { text: num + ' ' + unit, fill: '#3987e5',\n font: '600 26px Inter, system-ui, sans-serif' } },\n ],\n};\n", + "renderer": "canvas" }, - "pluginVersion": "12.4.6", + "pluginVersion": "7.2.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "expr": "rate(moqx_quicBytesRead_total[$__rate_interval]) * 8", - "legendFormat": "in", + "expr": "rate(moqx_quicBytesRead_total[10s]) * 8", + "interval": "3s", "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "expr": "rate(moqx_quicBytesWritten_total[$__rate_interval]) * 8", - "legendFormat": "out", - "refId": "B" } ], - "title": "BW in / out", - "type": "stat" + "title": "Ingress BW", + "transparent": false, + "type": "volkovlabs-echarts-panel" }, { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "PLACEHOLDER \u2014 upstream/mesh relays this node peers with. Currently lists only this relay; populates once peer relays are exported from /state (downstream_peers) or mesh config. No scoping on click yet.", "fieldConfig": { "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "cellOptions": { - "type": "auto" - }, - "filterable": false, - "footer": { - "reducers": [] - }, - "inspect": false - }, "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "text", - "value": 0 - } - ] - } + "unit": "bps" }, "overrides": [] }, "gridPos": { - "h": 8, - "w": 7, - "x": 0, - "y": 10 + "h": 4, + "w": 4, + "x": 20, + "y": 6 }, - "id": 23, + "id": 115, + "maxDataPoints": 500, "options": { - "cellHeight": "sm", - "showHeader": true, - "sortBy": [] + "editor": { + "format": "auto", + "height": 600 + }, + "editorMode": "code", + "getOption": "\nconst frames = context.panel.data.series || [];\nconst f = frames[0];\nfunction pairs(fr) {\n if (!fr) return [];\n const t = fr.fields.find((x) => x.type === 'time');\n const v = fr.fields.find((x) => x.type === 'number');\n if (!t || !v) return [];\n const tv = t.values.toArray ? t.values.toArray() : t.values;\n const vv = v.values.toArray ? v.values.toArray() : v.values;\n const out = [];\n for (let i = 0; i < tv.length; i++) {\n if (vv[i] != null && !Number.isNaN(vv[i])) out.push([tv[i], vv[i]]);\n }\n return out;\n}\nconst data = pairs(f);\nconst last = data.length ? data[data.length - 1][1] : 0;\nfunction fmt(bps) {\n const u = ['b/s', 'kb/s', 'Mb/s', 'Gb/s', 'Tb/s'];\n let i = 0, v = bps;\n while (v >= 1000 && i < u.length - 1) { v /= 1000; i++; }\n return [v.toFixed(1), u[i]];\n}\nconst [num, unit] = fmt(last);\nreturn {\n animation: false,\n grid: { left: -2, right: -2, top: '46%', bottom: -2 },\n xAxis: { type: 'time', show: false },\n yAxis: { type: 'value', show: false, scale: true, min: 0 },\n series: [{\n type: 'line', data, smooth: 0.1, showSymbol: false, silent: true,\n lineStyle: { color: '#C2562F', width: 1.8, cap: 'round' },\n areaStyle: { color: '#C2562F', opacity: 0.20 },\n emphasis: { disabled: true }, animation: false,\n }],\n graphic: [\n { type: 'text', left: 'center', top: 16,\n style: { text: num + ' ' + unit, fill: '#3987e5',\n font: '600 26px Inter, system-ui, sans-serif' } },\n ],\n};\n", + "renderer": "canvas" }, - "pluginVersion": "12.4.6", + "pluginVersion": "7.2.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "expr": "moqx_relay_active_sessions", - "format": "table", - "instant": true, - "range": false, + "expr": "rate(moqx_quicBytesWritten_total[10s]) * 8", + "interval": "3s", "refId": "A" } ], - "title": "Peer Relays", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true, - "__name__": true, - "instance": true, - "job": true - }, - "indexByName": {}, - "renameByName": { - "Value": "sessions", - "relay_id": "relay" - } - } - } - ], - "type": "table" + "title": "Egress BW", + "transparent": false, + "type": "volkovlabs-echarts-panel" }, { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "description": "One row per namespace. Click a namespace to scope the tracks table and the subscription graph below.", + "description": "One row per namespace \u2014 a leaf namespace typically maps to a single published event. Active counts tracks with at least one subscriber.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { - "align": "auto", + "align": "center", "cellOptions": { "type": "auto" }, @@ -1286,6 +1091,7 @@ "footer": { "reducers": [] }, + "headerAlign": "center", "inspect": false }, "mappings": [], @@ -1299,12 +1105,69 @@ ] } }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Namespace" + }, + "properties": [ + { + "id": "custom.width", + "value": 220 + }, + { + "id": "custom.align", + "value": "left" + }, + { + "id": "custom.headerAlign", + "value": "left" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Tracks" + }, + "properties": [ + { + "id": "custom.width", + "value": 80 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "custom.width", + "value": 80 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Subscribers" + }, + "properties": [ + { + "id": "custom.width", + "value": 110 + } + ] + } + ] }, "gridPos": { "h": 8, - "w": 7, - "x": 7, + "w": 10, + "x": 0, "y": 10 }, "id": 106, @@ -1325,7 +1188,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "sum by (namespace) (max by (namespace, track) (moqx_track_subscribers))", + "expr": "count by (namespace) (max by (namespace, track) (moqx_track_subscribers)) or label_replace(vector(0), \"namespace\", \"\u2014\", \"\", \"\")", "format": "table", "instant": true, "range": false, @@ -1336,11 +1199,22 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "count by (namespace) (max by (namespace, track) (moqx_track_subscribers))", + "expr": "count by (namespace) (max by (namespace, track) (moqx_track_subscribers) > 0)", "format": "table", "instant": true, "range": false, "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum by (namespace) (max by (namespace, track) (moqx_track_subscribers))", + "format": "table", + "instant": true, + "range": false, + "refId": "C" } ], "title": "Namespaces", @@ -1358,14 +1232,40 @@ "excludeByName": { "Time": true, "Time 1": true, - "Time 2": true + "Time 2": true, + "Time 3": true + }, + "indexByName": { + "Value #A": 1, + "Value #B": 2, + "Value #C": 3, + "namespace": 0 }, - "indexByName": {}, "renameByName": { - "Value #A": "subscribers", - "Value #B": "tracks" + "Value #A": "Tracks", + "Value #B": "Active", + "Value #C": "Subscribers", + "namespace": "Namespace" } } + }, + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "equal", + "options": { + "value": "\u2014" + } + }, + "fieldName": "Namespace" + } + ], + "match": "any", + "type": "exclude" + } } ], "type": "table" @@ -1375,21 +1275,19 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "One row per track, scoped by the namespace selection. Click a track to scope the subscription graph.", + "description": "Per-track view. Subs, Peak and Status come from relay metrics. Type/Codec/Resolution/FPS/Bitrate are catalog attributes and QoS/QoE need client-side telemetry; both report \u2014 until those sources exist.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { - "align": "auto", + "align": "center", "cellOptions": { "type": "auto" }, "filterable": false, - "footer": { - "reducers": [] - }, + "headerAlign": "center", "inspect": false }, "mappings": [], @@ -1403,12 +1301,261 @@ ] } }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Name" + }, + "properties": [ + { + "id": "custom.align", + "value": "left" + }, + { + "id": "custom.headerAlign", + "value": "left" + }, + { + "id": "links", + "value": [ + { + "title": "Scope to this track", + "url": "/grafana/d/moqx-overview/moqx-relay-overview?var-track=${__data.fields.Name}" + } + ] + }, + { + "id": "custom.width", + "value": 150 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Type" + }, + "properties": [ + { + "id": "custom.width", + "value": 58 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Codec" + }, + "properties": [ + { + "id": "custom.width", + "value": 72 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Resolution" + }, + "properties": [ + { + "id": "custom.width", + "value": 88 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "FPS" + }, + "properties": [ + { + "id": "custom.width", + "value": 48 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Bitrate" + }, + "properties": [ + { + "id": "custom.width", + "value": 74 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Subs" + }, + "properties": [ + { + "id": "custom.width", + "value": 54 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Peak" + }, + "properties": [ + { + "id": "custom.width", + "value": 54 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "QoS" + }, + "properties": [ + { + "id": "custom.align", + "value": "center" + }, + { + "id": "custom.cellOptions", + "value": { + "colorMode": "mapped", + "type": "pill" + } + }, + { + "id": "mappings", + "value": [ + { + "options": { + "DEGRADED": { + "color": "#c98500", + "index": 1 + }, + "FAILING": { + "color": "#e0453c", + "index": 2 + }, + "NORMAL": { + "color": "#1baf7a", + "index": 0 + } + }, + "type": "value" + } + ] + }, + { + "id": "custom.width", + "value": 64 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "QoE" + }, + "properties": [ + { + "id": "custom.align", + "value": "center" + }, + { + "id": "custom.cellOptions", + "value": { + "colorMode": "mapped", + "type": "pill" + } + }, + { + "id": "mappings", + "value": [ + { + "options": { + "DEGRADED": { + "color": "#c98500", + "index": 1 + }, + "FAILING": { + "color": "#e0453c", + "index": 2 + }, + "NORMAL": { + "color": "#1baf7a", + "index": 0 + } + }, + "type": "value" + } + ] + }, + { + "id": "custom.width", + "value": 64 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Status" + }, + "properties": [ + { + "id": "custom.align", + "value": "center" + }, + { + "id": "custom.cellOptions", + "value": { + "colorMode": "mapped", + "type": "pill" + } + }, + { + "id": "mappings", + "value": [ + { + "options": { + "DONE": { + "color": "#6e7680", + "index": 1 + }, + "LIVE": { + "color": "#1baf7a", + "index": 0 + }, + "OFFLINE": { + "color": "#6e7680", + "index": 2 + } + }, + "type": "value" + } + ] + }, + { + "id": "custom.width", + "value": 68 + } + ] + } + ] }, "gridPos": { "h": 8, - "w": 10, - "x": 14, + "w": 14, + "x": 10, "y": 10 }, "id": 24, @@ -1418,7 +1565,7 @@ "sortBy": [ { "desc": true, - "displayName": "subscribers" + "displayName": "Subs" } ] }, @@ -1429,30 +1576,93 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "max by (namespace, track) (moqx_track_subscribers)", + "expr": "label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(max by (namespace, track) (moqx_track_subscribers), \"type\", \"\u2014\", \"\", \"\"), \"codec\", \"\u2014\", \"\", \"\"), \"resolution\", \"\u2014\", \"\", \"\"), \"fps\", \"\u2014\", \"\", \"\"), \"bitrate\", \"\u2014\", \"\", \"\"), \"qos\", \"\u2014\", \"\", \"\"), \"qoe\", \"\u2014\", \"\", \"\"), \"status\", \"LIVE\", \"\", \"\") or label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace((max by (namespace, track) (max_over_time(moqx_track_subscribers[$__range])) unless max by (namespace, track) (moqx_track_subscribers)) * 0, \"type\", \"\u2014\", \"\", \"\"), \"codec\", \"\u2014\", \"\", \"\"), \"resolution\", \"\u2014\", \"\", \"\"), \"fps\", \"\u2014\", \"\", \"\"), \"bitrate\", \"\u2014\", \"\", \"\"), \"qos\", \"\u2014\", \"\", \"\"), \"qoe\", \"\u2014\", \"\", \"\"), \"status\", \"DONE\", \"\", \"\") or label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(vector(0), \"type\", \"\u2014\", \"\", \"\"), \"codec\", \"\u2014\", \"\", \"\"), \"resolution\", \"\u2014\", \"\", \"\"), \"fps\", \"\u2014\", \"\", \"\"), \"bitrate\", \"\u2014\", \"\", \"\"), \"qos\", \"\u2014\", \"\", \"\"), \"qoe\", \"\u2014\", \"\", \"\"), \"status\", \"\u2014\", \"\", \"\"), \"namespace\", \"\u2014\", \"\", \"\"), \"track\", \"\u2014\", \"\", \"\")", "format": "table", "instant": true, "range": false, "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "max by (namespace, track) (max_over_time(moqx_track_subscribers[$__range]))", + "format": "table", + "instant": true, + "range": false, + "refId": "B" } ], "title": "Tracks", "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "track", + "mode": "outer" + } + }, { "id": "organize", "options": { "excludeByName": { - "Time": true + "Time": true, + "Time 1": true, + "Time 2": true, + "__name__": true, + "data_tx": true, + "instance": true, + "job": true, + "namespace": true, + "namespace 1": true, + "namespace 2": true }, "indexByName": { - "namespace": 0, - "subscribers": 2, - "track": 1 + "Value #A": 6, + "Value #B": 7, + "bitrate": 5, + "codec": 2, + "fps": 4, + "qoe": 9, + "qos": 8, + "resolution": 3, + "status": 10, + "track": 0, + "type": 1 }, "renameByName": { - "Value": "subscribers" + "Value #A": "Subs", + "Value #B": "Peak", + "bitrate": "Bitrate", + "codec": "Codec", + "fps": "FPS", + "qoe": "QoE", + "qos": "QoS", + "resolution": "Resolution", + "status": "Status", + "track": "Name", + "type": "Type" } } + }, + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "equal", + "options": { + "value": "\u2014" + } + }, + "fieldName": "Name" + } + ], + "match": "any", + "type": "exclude" + } } ], "type": "table" @@ -1462,7 +1672,7 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Subscribers per track over time, scoped by the namespace/track selection (top 10 when unscoped). Click a legend entry to isolate.", + "description": "Subscribers per track for the ten busiest active tracks, ranked by their peak over the selected time range.", "fieldConfig": { "defaults": { "color": { @@ -1551,7 +1761,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "topk(10, max by (namespace, track) (moqx_track_subscribers))", + "expr": "max by (namespace, track) (moqx_track_subscribers > 0)\n and\ntopk(10, max by (namespace, track) (max_over_time(moqx_track_subscribers[$__range])))", "legendFormat": "{{namespace}}/{{track}}", "refId": "A" } diff --git a/docker/prometheus/prometheus.yml b/docker/prometheus/prometheus.yml index dc96e2796..d3ee7283e 100644 --- a/docker/prometheus/prometheus.yml +++ b/docker/prometheus/prometheus.yml @@ -56,11 +56,11 @@ scrape_configs: static_configs: - targets: ['localhost:9090'] - # Relay build info (version label for the host banner), via json_exporter - # scraping the admin /info. Effectively static — scraped coarsely. + # Relay build info for the banner badge. Cheap endpoint, so it is + # scraped often enough that a redeploy shows up promptly. - job_name: moqx-info metrics_path: /probe - scrape_interval: 60s + scrape_interval: 30s params: module: [moqx_info] static_configs: diff --git a/src/admin/BuiltinRoutes.cpp b/src/admin/BuiltinRoutes.cpp index a260194a8..f59dd9389 100644 --- a/src/admin/BuiltinRoutes.cpp +++ b/src/admin/BuiltinRoutes.cpp @@ -11,6 +11,7 @@ #include #include "admin/AdminServer.h" +#include "moqx/Version.h" namespace openmoq::moqx::admin { diff --git a/src/main.cpp b/src/main.cpp index 3d4ed117e..bde72de35 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,6 +15,7 @@ #include "bpf/QuicReuseportSteering.h" #include "config/loader/ConfigInit.h" #include "logging/LogSetup.h" +#include "moqx/Version.h" #include "stats/StatsRegistry.h" #include @@ -76,6 +77,8 @@ int main(int argc, char* argv[]) { " serve Start the relay (default)\n" + cfg::configSubcommandUsage() + "\nUsage: moqx [subcommand] --config " ); + // gflags handles --version inside folly::Init, before any config load. + google::SetVersionString(MOQX_VERSION); // MOQX_LOGGING is the moqx-namespaced alias for folly's own FOLLY_LOGGING env // var (folly::Init reads FOLLY_LOGGING). Promote it here — before folly::Init // — so the knob works for any launch method (docker, systemd, bare metal) @@ -89,6 +92,9 @@ int main(int argc, char* argv[]) { combineLoggingArgs(argc, argv); folly::Init init(&argc, &argv, true); + // Attributes the log stream to an exact build. + XLOG(INFO) << "moqx " << kVersion << " starting"; + std::string_view subcommand = kServeCommand; if (argc > 1) { subcommand = argv[1];