From 16481a1b14cfee1acb402bc85e6e146d1a7bb708 Mon Sep 17 00:00:00 2001 From: Giovanni Marzot Date: Thu, 23 Jul 2026 20:44:45 -0400 Subject: [PATCH 1/9] version: bake git describe into /info so the dashboard badge tracks releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The relay reported the static CMake project version (0.1.0) forever — tags never touched it. Now the docker publish job computes `git describe --tags` (bare tag on release commits, e.g. v0.2.1; tag-distance-sha on snapshots, e.g. v0.2.1-14-g2ed29c8) and passes it through a build arg into MOQX_VERSION. Local builds keep the static fallback. The dashboard badge picks it up via the existing /info -> json-exporter -> moqx_build_info chain, no dashboard changes. Publish checkout needs fetch-depth 0 for describe to see tags. NOTE: the Dockerfile path is only exercised by the main publish job — watch the first post-merge publish. --- .github/workflows/ci-main.yml | 7 +++++++ CMakeLists.txt | 9 ++++++++- docker/Dockerfile | 6 ++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 309cf55b1..899a919ad 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: @@ -379,12 +382,16 @@ jobs: fi echo "short=$SHORT" >> "$GITHUB_OUTPUT" echo "rolling=${LABEL}-latest" >> "$GITHUB_OUTPUT" + # Version baked into the binary's /info (relay dashboard badge): + # exact tag on release commits, tag-distance-sha on snapshots. + echo "describe=$(git describe --tags --always)" >> "$GITHUB_OUTPUT" - name: Build Docker image run: | IMAGE="ghcr.io/${{ github.repository }}" ARCH="${{ matrix.arch }}" docker build -f docker/Dockerfile \ + --build-arg MOQX_VERSION_STRING="${{ steps.tags.outputs.describe }}" \ -t "${IMAGE}:${{ steps.tags.outputs.short }}-${ARCH}" \ -t "${IMAGE}:${{ steps.tags.outputs.rolling }}-${ARCH}" \ . diff --git a/CMakeLists.txt b/CMakeLists.txt index 1039741fd..4b5e16bd6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,7 +202,14 @@ 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}") +# Version reported by /info (and the relay dashboard badge). CI passes +# `git describe --tags` here: the bare tag on release commits (v0.2.1), +# tag-distance-sha on snapshots (v0.2.1-14-g2ed29c8). Local builds fall +# back to the static project version. +if(NOT MOQX_VERSION_STRING) + set(MOQX_VERSION_STRING "${PROJECT_VERSION}") +endif() +target_compile_definitions(moqx_core PRIVATE MOQX_VERSION="${MOQX_VERSION_STRING}") # --- Config types (resolved Config structs, header-only, no rfl dependency) --- # Library consumers that only need the Config API link this. diff --git a/docker/Dockerfile b/docker/Dockerfile index 6db4e514d..377b4c42a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,8 +20,14 @@ COPY src/ src/ COPY deps/catapult/ deps/catapult/ COPY deps/moxygen/build/fbcode_builder/CMake deps/moxygen/build/fbcode_builder/CMake +# Version string baked into /info — CI passes `git describe --tags` (the +# build context has no .git, so it can't be derived here). Empty means the +# CMake fallback (static project version) applies. +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 From 9b672b4100b62b88c07929142ef5a7f123098f7d Mon Sep 17 00:00:00 2001 From: Giovanni Marzot Date: Fri, 24 Jul 2026 10:13:37 -0400 Subject: [PATCH 2/9] version: strip leading v from describe (badge adds its own; /info stays bare semver-style) --- .github/workflows/ci-main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 899a919ad..106ddc12c 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -384,7 +384,9 @@ jobs: echo "rolling=${LABEL}-latest" >> "$GITHUB_OUTPUT" # Version baked into the binary's /info (relay dashboard badge): # exact tag on release commits, tag-distance-sha on snapshots. - echo "describe=$(git describe --tags --always)" >> "$GITHUB_OUTPUT" + # Leading "v" stripped — /info reports bare semver-style ("0.2.1", + # "0.2.1-14-gabc1234"); display layers add their own "v". + echo "describe=$(git describe --tags --always | sed 's/^v//')" >> "$GITHUB_OUTPUT" - name: Build Docker image run: | From b5fde4b2d73664a6c0973446abb44d482d78e59b Mon Sep 17 00:00:00 2001 From: Giovanni Marzot Date: Sun, 26 Jul 2026 20:31:14 -0400 Subject: [PATCH 3/9] version: derive in cmake, not docker; filter non-release tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review on #505. Why docker-only was wrong: the version was a docker --build-arg, so only published images carried a real one — local builds, release tarballs and the artifact tarball all reported the static project version (0.1.0). Derivation now lives in cmake/MoqxVersion.cmake with an explicit precedence chain: 1. -DMOQX_VERSION_STRING explicit (release workflow passes the tag) 2. /VERSION stamped by CI; docker + source tarballs have no .git, so this is how they resolve 3. git describe any clone, including local dev builds 4. v${PROJECT_VERSION} last resort Tag filtering is load-bearing. The repo carries moving non-version tags (snapshot-latest, build-*, archive/*); an unfiltered describe returns "snapshot-latest" as the version of every image built from main — demonstrated on 4969f8c5, where naive describe gives snapshot-latest and --match 'v[0-9]*' gives the sha. The v prefix is kept: the string is a git identifier and round-trips into git checkout / gh release view. Generalization payoff, all verified in a container build: - moqx --version -> moqx version v0.2.1-2-g59fe6a9b - admin /info -> {"service":"moqx","version":"v0.2.1-2-g59fe6a9b"} - startup log banner -> first line attributes the log stream to a build - /usr/local/VERSION -> identify an image without executing it - install tree VERSION -> release + snapshot tarballs carry the id The version is now an interface target (moqx_version) carrying a generated header, so any target can use it — not a private compile definition on moqx_core. --- .github/workflows/ci-main.yml | 20 ++++-- .github/workflows/version-release.yml | 5 ++ .gitignore | 3 + CMakeLists.txt | 22 ++++--- cmake/MoqxVersion.cmake | 92 +++++++++++++++++++++++++++ cmake/Version.h.in | 25 ++++++++ docker/Dockerfile | 19 ++++-- src/admin/BuiltinRoutes.cpp | 1 + src/main.cpp | 9 +++ 9 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 cmake/MoqxVersion.cmake create mode 100644 cmake/Version.h.in diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 106ddc12c..c041cd903 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -382,18 +382,26 @@ jobs: fi echo "short=$SHORT" >> "$GITHUB_OUTPUT" echo "rolling=${LABEL}-latest" >> "$GITHUB_OUTPUT" - # Version baked into the binary's /info (relay dashboard badge): - # exact tag on release commits, tag-distance-sha on snapshots. - # Leading "v" stripped — /info reports bare semver-style ("0.2.1", - # "0.2.1-14-gabc1234"); display layers add their own "v". - echo "describe=$(git describe --tags --always | sed 's/^v//')" >> "$GITHUB_OUTPUT" + + - name: Stamp build version + run: | + # Single source of truth for the version baked into the binary + # (/info, --version, log banner) and shipped in artifacts. The + # docker build context excludes .git, so cmake reads this file — + # see cmake/MoqxVersion.cmake for the full resolution order. + # + # --match 'v[0-9]*' is load-bearing: the repo also carries moving, + # non-version tags (snapshot-latest, build-*, archive/*) and an + # unfiltered describe returns e.g. "snapshot-latest" as the version + # of every image built from main. + git describe --tags --match 'v[0-9]*' --always > VERSION + echo "==> build version: $(cat VERSION)" - name: Build Docker image run: | IMAGE="ghcr.io/${{ github.repository }}" ARCH="${{ matrix.arch }}" docker build -f docker/Dockerfile \ - --build-arg MOQX_VERSION_STRING="${{ steps.tags.outputs.describe }}" \ -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..b61dba4b1 100644 --- a/.github/workflows/version-release.yml +++ b/.github/workflows/version-release.yml @@ -324,8 +324,13 @@ 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. + # Stamp the exact release tag rather than deriving it: this job's + # checkout is shallow (no tags) and the tag may not exist yet when + # the build runs, so `git describe` would yield a bare sha. See + # cmake/MoqxVersion.cmake. 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..29dd073f2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ docker/cloudflare.ini # gh-pages dashboard (lives on gh-pages branch, not main) /gh-pages/ docker/grafana/provisioning/dashboards/archive/ + +# Build-version stamp written by CI (cmake/MoqxVersion.cmake reads it). +/VERSION diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b5e16bd6..221552125 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,11 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "" FORCE) endif() +# Build identifier (git describe / VERSION file / -DMOQX_VERSION_STRING). +# Defines the moqx_version interface target carrying the generated +# moqx/Version.h, and writes ${CMAKE_BINARY_DIR}/VERSION for packaging. +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,14 +207,10 @@ target_link_libraries(moqx_core PUBLIC ) target_compile_options(moqx_core PRIVATE -Wall -Wextra -Wpedantic) -# Version reported by /info (and the relay dashboard badge). CI passes -# `git describe --tags` here: the bare tag on release commits (v0.2.1), -# tag-distance-sha on snapshots (v0.2.1-14-g2ed29c8). Local builds fall -# back to the static project version. -if(NOT MOQX_VERSION_STRING) - set(MOQX_VERSION_STRING "${PROJECT_VERSION}") -endif() -target_compile_definitions(moqx_core PRIVATE MOQX_VERSION="${MOQX_VERSION_STRING}") +# Build identifier — see cmake/MoqxVersion.cmake. PUBLIC so consumers (the +# moqx binary, tests, tools) can include moqx/Version.h too: the version is +# no longer a private compile definition on this one library. +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. @@ -326,3 +327,8 @@ endif() include(${PROJECT_SOURCE_DIR}/cmake/Lint.cmake) install(TARGETS moqx moqx-issuer moqx_core moqx_config_loader) + +# Ship the build identifier alongside the binaries so an unpacked release or +# snapshot tarball can be identified with `cat VERSION` — no git history, and +# no need to run the binary (wrong arch, no libs, or a container layer). +install(FILES ${CMAKE_BINARY_DIR}/VERSION DESTINATION .) diff --git a/cmake/MoqxVersion.cmake b/cmake/MoqxVersion.cmake new file mode 100644 index 000000000..a20d62d84 --- /dev/null +++ b/cmake/MoqxVersion.cmake @@ -0,0 +1,92 @@ +# Version derivation for moqx. +# +# Produces MOQX_VERSION_STRING: one canonical identifier consumed by every +# binary (admin /info, --version, the startup log banner) and embedded in +# build artifacts as a VERSION file, so a tarball or container can be +# identified without its git history. +# +# Format is git-describe output with the "v" tag prefix retained, so the +# string round-trips into `git checkout` / `gh release view`: +# +# v0.2.1 exact release tag +# v0.2.1-14-gabc1234 14 commits past v0.2.1 +# v0.2.1-14-gabc1234-dirty ...with uncommitted changes (local builds) +# abc1234 no reachable v* tag (shallow clone, fork) +# +# Resolution order, first hit wins: +# +# 1. -DMOQX_VERSION_STRING=... explicit override (CI, docker --build-arg) +# 2. /VERSION source tarballs and the docker build context +# carry no .git; CI writes this file for them +# 3. git describe any real clone, including local dev builds +# 4. v${PROJECT_VERSION} last resort, e.g. v0.1.0 +# +# Only v-prefixed numeric tags are considered. The repo also carries moving +# tags (snapshot-latest, build-*, archive/*) that are NOT versions; an +# unfiltered `git describe` happily returns "snapshot-latest" and poisons +# the version of every artifact built from main. +# +# Derivation happens at configure time. A dev who commits without re-running +# cmake keeps the previous string until the next configure; CI configures +# fresh every run, so published artifacts are always exact. + +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 — EXISTS covers both. + if(NOT EXISTS "${PROJECT_SOURCE_DIR}/.git") + return() + endif() + + execute_process( + COMMAND "${GIT_EXECUTABLE}" describe --tags --match "v[0-9]*" --always --dirty + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + OUTPUT_VARIABLE _described + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + RESULT_VARIABLE _rc + ) + if(_rc EQUAL 0 AND _described) + set(${out_var} "${_described}" PARENT_SCOPE) + endif() +endfunction() + +if(MOQX_VERSION_STRING) + set(_moqx_version_source "explicit -DMOQX_VERSION_STRING") +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() + _moqx_version_from_git(MOQX_VERSION_STRING) + if(MOQX_VERSION_STRING) + set(_moqx_version_source "git describe") + 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})") + +# Generated header — any target may link moqx_version to use it, so the +# version is not welded to one library the way a target_compile_definitions +# would be. +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 +) + +add_library(moqx_version INTERFACE) +target_include_directories(moqx_version INTERFACE "${MOQX_VERSION_HEADER_DIR}") + +# Artifact-side copy: `cat VERSION` identifies an unpacked tarball or an +# image layer with no binary to run and no git history to consult. +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 377b4c42a..7d658433a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,16 +13,21 @@ WORKDIR /src # Moxygen tarball (pre-extracted bookworm build) COPY .docker-deps/moxygen /opt/moxygen -# MoQX source — only what cmake needs -COPY CMakeLists.txt CMakePresets.json ./ +# MoQX source — only what cmake needs. +# VERSIO[N] is an optional-copy glob: CI stamps a VERSION file into the build +# context (the context excludes .git, so cmake can't run git describe here), +# but a bare `docker build` has none and must still work. The glob matches +# nothing in that case, and the COPY still succeeds because CMakeLists.txt +# does — a plain `COPY VERSION` would hard-fail instead. +COPY CMakeLists.txt CMakePresets.json VERSIO[N] ./ COPY cmake/ cmake/ COPY src/ src/ COPY deps/catapult/ deps/catapult/ COPY deps/moxygen/build/fbcode_builder/CMake deps/moxygen/build/fbcode_builder/CMake -# Version string baked into /info — CI passes `git describe --tags` (the -# build context has no .git, so it can't be derived here). Empty means the -# CMake fallback (static project version) applies. +# Explicit override for one-off builds; normally empty, in which case cmake +# falls through to the VERSION file copied above (then to the static project +# version if that is absent too). See cmake/MoqxVersion.cmake. ARG MOQX_VERSION_STRING="" RUN cmake -S . -B _build --preset default \ @@ -44,6 +49,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ COPY --from=builder /install/bin/moqx /usr/local/bin/moqx COPY --from=builder /install/bin/moqx-issuer /usr/local/bin/moqx-issuer +# Build identifier as a plain file, so an image can be identified without +# executing it — inspecting a layer, or a foreign-arch image that cannot run +# here. `docker run --rm moqx --version` reports the same string. +COPY --from=builder /install/VERSION /usr/local/VERSION COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh COPY docker/config.docker.yaml /usr/local/share/moqx/config.docker.yaml RUN chmod +x /usr/local/bin/entrypoint.sh 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..327d46e99 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,10 @@ int main(int argc, char* argv[]) { " serve Start the relay (default)\n" + cfg::configSubcommandUsage() + "\nUsage: moqx [subcommand] --config " ); + // Powers `moqx --version` (gflags handles the flag inside folly::Init). + // Works with no config file present, so an image or tarball can be + // identified without deploying it. + 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 +94,10 @@ int main(int argc, char* argv[]) { combineLoggingArgs(argc, argv); folly::Init init(&argc, &argv, true); + // First line in every relay's log: attributes the whole log stream to an + // exact build (see cmake/MoqxVersion.cmake for the identifier's format). + XLOG(INFO) << "moqx " << kVersion << " starting"; + std::string_view subcommand = kServeCommand; if (argc > 1) { subcommand = argv[1]; From baef9baf9a698e9e567af436678b8ac50b565b6b Mon Sep 17 00:00:00 2001 From: Giovanni Marzot Date: Mon, 27 Jul 2026 13:50:32 -0400 Subject: [PATCH 4/9] version: let git outrank a stale VERSION stamp; ignore /_build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The VERSION file is gitignored, so a stamp left in a working tree (e.g. after reproducing a CI build by hand) is invisible to git status and silently pinned every later build to a dead version. git describe now takes precedence whenever .git exists; the contexts that actually need the file — docker build context, source tarballs — have no .git and are unaffected. Verified: clone+stale file -> git describe wins; -DMOQX_VERSION_STRING still outranks both; a .git-less tree still reads the file. Also ignore /_build, the directory docker/Dockerfile and version-release.yml configure into — same hand-reproduced-CI scenario. --- .gitignore | 3 +++ cmake/MoqxVersion.cmake | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 29dd073f2..ebe021964 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Build outputs /build/ /build-*/ +# docker/Dockerfile and version-release.yml build here; only reachable +# locally when reproducing a CI build by hand. +/_build/ /cmake-build-*/ /CMakeFiles/ CMakeCache.txt diff --git a/cmake/MoqxVersion.cmake b/cmake/MoqxVersion.cmake index a20d62d84..53b8cfe4a 100644 --- a/cmake/MoqxVersion.cmake +++ b/cmake/MoqxVersion.cmake @@ -16,11 +16,17 @@ # Resolution order, first hit wins: # # 1. -DMOQX_VERSION_STRING=... explicit override (CI, docker --build-arg) -# 2. /VERSION source tarballs and the docker build context +# 2. git describe any real clone, including local dev builds +# 3. /VERSION source tarballs and the docker build context # carry no .git; CI writes this file for them -# 3. git describe any real clone, including local dev builds # 4. v${PROJECT_VERSION} last resort, e.g. v0.1.0 # +# git outranks the VERSION file deliberately. The file is gitignored, so a +# stale stamp left in a working tree (say, from reproducing a CI build +# locally) is invisible to git status and would otherwise pin every later +# build to a dead version. The contexts that genuinely need the file have no +# .git at all, so they are unaffected by the ordering. +# # Only v-prefixed numeric tags are considered. The repo also carries moving # tags (snapshot-latest, build-*, archive/*) that are NOT versions; an # unfiltered `git describe` happily returns "snapshot-latest" and poisons @@ -58,14 +64,14 @@ endfunction() if(MOQX_VERSION_STRING) set(_moqx_version_source "explicit -DMOQX_VERSION_STRING") -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() _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") From f4d6b2eb30c47a23e3a6a9f98f361a17d97c8a1e Mon Sep 17 00:00:00 2001 From: Giovanni Marzot Date: Wed, 29 Jul 2026 08:01:07 -0400 Subject: [PATCH 5/9] version: untracked-aware dirty, OCI image labels, trim comments - describe --dirty only inspects tracked files; add a git status check so a tree with new files is not reported as a clean commit - images carry org.opencontainers.image.* labels and take the version via build-arg; drop the in-image VERSION file and the optional-copy glob - the VERSION file remains for tarballs, which have no label mechanism - trim comments per review --- .github/workflows/ci-main.yml | 21 +++---- .github/workflows/version-release.yml | 6 +- .gitignore | 4 +- CMakeLists.txt | 12 +--- cmake/MoqxVersion.cmake | 79 +++++++++++++-------------- docker/Dockerfile | 28 +++++----- src/main.cpp | 7 +-- 7 files changed, 69 insertions(+), 88 deletions(-) diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index c041cd903..cfad07e13 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -383,25 +383,22 @@ jobs: echo "short=$SHORT" >> "$GITHUB_OUTPUT" echo "rolling=${LABEL}-latest" >> "$GITHUB_OUTPUT" - - name: Stamp build version + - name: Resolve build version + id: version run: | - # Single source of truth for the version baked into the binary - # (/info, --version, log banner) and shipped in artifacts. The - # docker build context excludes .git, so cmake reads this file — - # see cmake/MoqxVersion.cmake for the full resolution order. - # - # --match 'v[0-9]*' is load-bearing: the repo also carries moving, - # non-version tags (snapshot-latest, build-*, archive/*) and an - # unfiltered describe returns e.g. "snapshot-latest" as the version - # of every image built from main. - git describe --tags --match 'v[0-9]*' --always > VERSION - echo "==> build version: $(cat VERSION)" + # 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 b61dba4b1..0aadec0cf 100644 --- a/.github/workflows/version-release.yml +++ b/.github/workflows/version-release.yml @@ -324,10 +324,8 @@ 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. - # Stamp the exact release tag rather than deriving it: this job's - # checkout is shallow (no tags) and the tag may not exist yet when - # the build runs, so `git describe` would yield a bare sha. See - # cmake/MoqxVersion.cmake. + # 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 }}" \ diff --git a/.gitignore b/.gitignore index ebe021964..c13cb2ac3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ # Build outputs /build/ /build-*/ -# docker/Dockerfile and version-release.yml build here; only reachable -# locally when reproducing a CI build by hand. /_build/ /cmake-build-*/ /CMakeFiles/ @@ -43,5 +41,5 @@ docker/cloudflare.ini /gh-pages/ docker/grafana/provisioning/dashboards/archive/ -# Build-version stamp written by CI (cmake/MoqxVersion.cmake reads it). +# Version stamp for source trees with no .git (cmake/MoqxVersion.cmake). /VERSION diff --git a/CMakeLists.txt b/CMakeLists.txt index 221552125..7f3d200a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,9 +11,7 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "" FORCE) endif() -# Build identifier (git describe / VERSION file / -DMOQX_VERSION_STRING). -# Defines the moqx_version interface target carrying the generated -# moqx/Version.h, and writes ${CMAKE_BINARY_DIR}/VERSION for packaging. +# 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) @@ -207,9 +205,7 @@ target_link_libraries(moqx_core PUBLIC ) target_compile_options(moqx_core PRIVATE -Wall -Wextra -Wpedantic) -# Build identifier — see cmake/MoqxVersion.cmake. PUBLIC so consumers (the -# moqx binary, tests, tools) can include moqx/Version.h too: the version is -# no longer a private compile definition on this one library. +# 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) --- @@ -328,7 +324,5 @@ include(${PROJECT_SOURCE_DIR}/cmake/Lint.cmake) install(TARGETS moqx moqx-issuer moqx_core moqx_config_loader) -# Ship the build identifier alongside the binaries so an unpacked release or -# snapshot tarball can be identified with `cat VERSION` — no git history, and -# no need to run the binary (wrong arch, no libs, or a container layer). +# 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 index 53b8cfe4a..327f972f2 100644 --- a/cmake/MoqxVersion.cmake +++ b/cmake/MoqxVersion.cmake @@ -1,40 +1,23 @@ -# Version derivation for moqx. +# Build identifier compiled into the binary (/info, --version, log banner) and +# written to ${CMAKE_BINARY_DIR}/VERSION for packaging. # -# Produces MOQX_VERSION_STRING: one canonical identifier consumed by every -# binary (admin /info, --version, the startup log banner) and embedded in -# build artifacts as a VERSION file, so a tarball or container can be -# identified without its git history. +# 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: # -# Format is git-describe output with the "v" tag prefix retained, so the -# string round-trips into `git checkout` / `gh release view`: +# 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} # -# v0.2.1 exact release tag -# v0.2.1-14-gabc1234 14 commits past v0.2.1 -# v0.2.1-14-gabc1234-dirty ...with uncommitted changes (local builds) -# abc1234 no reachable v* tag (shallow clone, fork) +# git outranks the VERSION file because that file is gitignored: a stale one +# left in a working tree would silently pin every later build. # -# Resolution order, first hit wins: +# 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. # -# 1. -DMOQX_VERSION_STRING=... explicit override (CI, docker --build-arg) -# 2. git describe any real clone, including local dev builds -# 3. /VERSION source tarballs and the docker build context -# carry no .git; CI writes this file for them -# 4. v${PROJECT_VERSION} last resort, e.g. v0.1.0 -# -# git outranks the VERSION file deliberately. The file is gitignored, so a -# stale stamp left in a working tree (say, from reproducing a CI build -# locally) is invisible to git status and would otherwise pin every later -# build to a dead version. The contexts that genuinely need the file have no -# .git at all, so they are unaffected by the ordering. -# -# Only v-prefixed numeric tags are considered. The repo also carries moving -# tags (snapshot-latest, build-*, archive/*) that are NOT versions; an -# unfiltered `git describe` happily returns "snapshot-latest" and poisons -# the version of every artifact built from main. -# -# Derivation happens at configure time. A dev who commits without re-running -# cmake keeps the previous string until the next configure; CI configures -# fresh every run, so published artifacts are always exact. +# 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) @@ -44,22 +27,38 @@ function(_moqx_version_from_git out_var) return() endif() - # A worktree's .git is a file, not a directory — EXISTS covers both. + # 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 --dirty + 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(_rc EQUAL 0 AND _described) - set(${out_var} "${_described}" PARENT_SCOPE) + 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) @@ -80,9 +79,6 @@ endif() message(STATUS "moqx version: ${MOQX_VERSION_STRING} (from ${_moqx_version_source})") -# Generated header — any target may link moqx_version to use it, so the -# version is not welded to one library the way a target_compile_definitions -# would be. set(MOQX_VERSION_HEADER_DIR "${CMAKE_BINARY_DIR}/generated") configure_file( "${PROJECT_SOURCE_DIR}/cmake/Version.h.in" @@ -90,9 +86,10 @@ configure_file( @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}") -# Artifact-side copy: `cat VERSION` identifies an unpacked tarball or an -# image layer with no binary to run and no git history to consult. +# 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/docker/Dockerfile b/docker/Dockerfile index 7d658433a..9acc4abb3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,21 +13,15 @@ WORKDIR /src # Moxygen tarball (pre-extracted bookworm build) COPY .docker-deps/moxygen /opt/moxygen -# MoQX source — only what cmake needs. -# VERSIO[N] is an optional-copy glob: CI stamps a VERSION file into the build -# context (the context excludes .git, so cmake can't run git describe here), -# but a bare `docker build` has none and must still work. The glob matches -# nothing in that case, and the COPY still succeeds because CMakeLists.txt -# does — a plain `COPY VERSION` would hard-fail instead. -COPY CMakeLists.txt CMakePresets.json VERSIO[N] ./ +# MoQX source — only what cmake needs +COPY CMakeLists.txt CMakePresets.json ./ COPY cmake/ cmake/ COPY src/ src/ COPY deps/catapult/ deps/catapult/ COPY deps/moxygen/build/fbcode_builder/CMake deps/moxygen/build/fbcode_builder/CMake -# Explicit override for one-off builds; normally empty, in which case cmake -# falls through to the VERSION file copied above (then to the static project -# version if that is absent too). See cmake/MoqxVersion.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 \ @@ -47,12 +41,18 @@ 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 -# Build identifier as a plain file, so an image can be identified without -# executing it — inspecting a layer, or a foreign-arch image that cannot run -# here. `docker run --rm moqx --version` reports the same string. -COPY --from=builder /install/VERSION /usr/local/VERSION COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh COPY docker/config.docker.yaml /usr/local/share/moqx/config.docker.yaml RUN chmod +x /usr/local/bin/entrypoint.sh diff --git a/src/main.cpp b/src/main.cpp index 327d46e99..bde72de35 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -77,9 +77,7 @@ int main(int argc, char* argv[]) { " serve Start the relay (default)\n" + cfg::configSubcommandUsage() + "\nUsage: moqx [subcommand] --config " ); - // Powers `moqx --version` (gflags handles the flag inside folly::Init). - // Works with no config file present, so an image or tarball can be - // identified without deploying it. + // 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 @@ -94,8 +92,7 @@ int main(int argc, char* argv[]) { combineLoggingArgs(argc, argv); folly::Init init(&argc, &argv, true); - // First line in every relay's log: attributes the whole log stream to an - // exact build (see cmake/MoqxVersion.cmake for the identifier's format). + // Attributes the log stream to an exact build. XLOG(INFO) << "moqx " << kVersion << " starting"; std::string_view subcommand = kServeCommand; From c7ec7ef4e6b99a056c508bf28cf1267ce0fabd47 Mon Sep 17 00:00:00 2001 From: Giovanni Marzot Date: Wed, 29 Jul 2026 08:23:12 -0400 Subject: [PATCH 6/9] grafana: media-oriented track table, latency p50/p99, sparkline polish Active Tracks gains media columns (type, codec, resolution, fps, bitrate, data tx) as constant labels until metrics exist, plus QoS/QoE/status pills. Latency reports p50 and p99 overlaid; loss/drops in violet, egress green; wider rate windows to cut sampling noise. Banner collapses to a one-line logo+version strip before going single-column. --- .../dashboards/moqx-overview.json | 364 +++++++++++++----- 1 file changed, 277 insertions(+), 87 deletions(-) diff --git a/docker/grafana/provisioning/dashboards/moqx-overview.json b/docker/grafana/provisioning/dashboards/moqx-overview.json index c9436826d..cd6ab2b85 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 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
", "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: 20px; }\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", @@ -889,22 +889,12 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "p95 QUIC RTT across all connections. Rising latency + rising loss = overload. Peak over the dashboard range.", + "description": "QUIC RTT: headline is current p50; p50 and p99 overlaid behind it.", "fieldConfig": { "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } + "decimals": 1, + "mappings": [], + "unit": "ms" }, "overrides": [] }, @@ -916,33 +906,21 @@ }, "id": 108, "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 + "getOption": "\nconst frames = context.panel.data.series || [];\nconst INK = '#e6edf3', MUTED = 'rgba(230,237,243,.45)';\nconst C = { p50: '#22b8cf', p99: '#7ae0ef' };\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\nfunction line(data, color, width, fill) {\n return {\n type: 'line', data, smooth: 0.4, 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 dot = (x, color) => ({\n type: 'circle', right: x, top: 8, shape: { cx: 0, cy: 0, r: 3 },\n style: { fill: color },\n});\nconst tag = (x, text) => ({\n type: 'text', right: x, top: 3,\n style: { text, fill: MUTED, font: '10px monospace' },\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: 10, top: 26,\n style: { text: fmt(last(p50)), fill: INK,\n font: '600 28px Inter, system-ui, sans-serif' } },\n { type: 'text', left: 10, top: 26,\n style: { text: '', fill: MUTED } },\n { type: 'text', left: 10 + String(fmt(last(p50))).length * 16, top: 38,\n style: { text: 'ms', fill: MUTED, font: '11px monospace' } },\n dot(74, C.p50), tag(46, 'p50'),\n dot(30, C.p99), tag(4, 'p99'),\n ],\n};\n", + "renderer": "canvas" }, - "pluginVersion": "6.3.0", "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[2m]))) >= 0 or vector(0)", + "legendFormat": "p50", "refId": "A" }, { @@ -950,24 +928,13 @@ "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[2m]))) >= 0 or vector(0)", + "legendFormat": "p99", "refId": "B" } ], "title": "Latency", - "transformations": [ - { - "id": "joinByField", - "options": { - "byField": "k", - "mode": "outer" - } - } - ], - "type": "marcusolsson-dynamictext-panel" + "type": "volkovlabs-echarts-panel" }, { "datasource": { @@ -978,7 +945,7 @@ "fieldConfig": { "defaults": { "color": { - "fixedColor": "#3987e5", + "fixedColor": "#8b7bea", "mode": "fixed" }, "decimals": 1, @@ -987,8 +954,8 @@ "mode": "absolute", "steps": [ { - "color": "#3987e5", - "value": 0 + "color": "#8b7bea", + "value": null } ] }, @@ -1012,27 +979,23 @@ { "id": "color", "value": { - "mode": "thresholds" + "fixedColor": "#8b7bea", + "mode": "fixed" } - }, + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "drops" + }, + "properties": [ { - "id": "thresholds", + "id": "color", "value": { - "mode": "absolute", - "steps": [ - { - "color": "#3987e5", - "value": 0 - }, - { - "color": "#c98500", - "value": 0.5 - }, - { - "color": "#e66767", - "value": 2 - } - ] + "fixedColor": "#c3b5f5", + "mode": "fixed" } } ] @@ -1070,7 +1033,7 @@ "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[2m])) / clamp_min(sum(rate(moqx_quicPacketsSent_total[2m])), 1)", "legendFormat": "loss", "refId": "A" }, @@ -1079,7 +1042,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "sum(rate(moqx_quicDatagramsDroppedOnRead_total[$__rate_interval])) + sum(rate(moqx_quicDatagramsDroppedOnWrite_total[$__rate_interval]))", + "expr": "sum(rate(moqx_quicDatagramsDroppedOnRead_total[2m])) + sum(rate(moqx_quicDatagramsDroppedOnWrite_total[2m]))", "legendFormat": "drops", "refId": "B" } @@ -1122,7 +1085,7 @@ { "id": "color", "value": { - "fixedColor": "#d95926", + "fixedColor": "#1baf7a", "mode": "fixed" } } @@ -1167,7 +1130,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "rate(moqx_quicBytesRead_total[$__rate_interval]) * 8", + "expr": "rate(moqx_quicBytesRead_total[2m]) * 8", "legendFormat": "in", "refId": "A" }, @@ -1176,7 +1139,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "rate(moqx_quicBytesWritten_total[$__rate_interval]) * 8", + "expr": "rate(moqx_quicBytesWritten_total[2m]) * 8", "legendFormat": "out", "refId": "B" } @@ -1221,7 +1184,7 @@ }, "gridPos": { "h": 8, - "w": 7, + "w": 6, "x": 0, "y": 10 }, @@ -1303,8 +1266,8 @@ }, "gridPos": { "h": 8, - "w": 7, - "x": 7, + "w": 9, + "x": 6, "y": 10 }, "id": 106, @@ -1375,7 +1338,7 @@ "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 media view. Columns without a metric behind them yet (type, codec, resolution, fps, bitrate, data tx) render as \u2014 and are wired as constant labels; QoS/QoE/status are placeholder pills.", "fieldConfig": { "defaults": { "color": { @@ -1403,12 +1366,192 @@ ] } }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "QoS" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "color-background" + } + }, + { + "id": "custom.align", + "value": "center" + }, + { + "id": "custom.width", + "value": 92 + }, + { + "id": "mappings", + "value": [ + { + "options": { + "DEGRADED": { + "color": "#c98500", + "index": 1 + }, + "FAILING": { + "color": "#e66767", + "index": 2 + }, + "NORMAL": { + "color": "#1baf7a", + "index": 0 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "QoE" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "color-background" + } + }, + { + "id": "custom.align", + "value": "center" + }, + { + "id": "custom.width", + "value": 92 + }, + { + "id": "mappings", + "value": [ + { + "options": { + "DEGRADED": { + "color": "#c98500", + "index": 1 + }, + "FAILING": { + "color": "#e66767", + "index": 2 + }, + "NORMAL": { + "color": "#1baf7a", + "index": 0 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Status" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "color-background" + } + }, + { + "id": "custom.align", + "value": "center" + }, + { + "id": "custom.width", + "value": 92 + }, + { + "id": "mappings", + "value": [ + { + "options": { + "DONE": { + "color": "#3987e5", + "index": 2 + }, + "LIVE": { + "color": "#1baf7a", + "index": 0 + }, + "OFFLINE": { + "color": "#6e7680", + "index": 1 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Stream" + }, + "properties": [ + { + "id": "custom.width", + "value": 190 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Subs" + }, + "properties": [ + { + "id": "custom.width", + "value": 70 + }, + { + "id": "custom.align", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Peak" + }, + "properties": [ + { + "id": "custom.width", + "value": 70 + }, + { + "id": "custom.align", + "value": "right" + } + ] + } + ] }, "gridPos": { "h": 8, - "w": 10, - "x": 14, + "w": 9, + "x": 15, "y": 10 }, "id": 24, @@ -1418,7 +1561,7 @@ "sortBy": [ { "desc": true, - "displayName": "subscribers" + "displayName": "Subs" } ] }, @@ -1429,28 +1572,75 @@ "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(label_replace(max by (namespace, track) (moqx_track_subscribers), \"type\", \"\u2014\", \"\", \"\"), \"codec\", \"\u2014\", \"\", \"\"), \"resolution\", \"\u2014\", \"\", \"\"), \"fps\", \"\u2014\", \"\", \"\"), \"bitrate\", \"\u2014\", \"\", \"\"), \"data_tx\", \"\u2014\", \"\", \"\"), \"qos\", \"NORMAL\", \"\", \"\"), \"qoe\", \"NORMAL\", \"\", \"\"), \"status\", \"LIVE\", \"\", \"\")", "format": "table", "instant": true, "range": false, "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "max_over_time((max by (namespace, track) (moqx_track_subscribers))[$__range:30s])", + "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, + "instance": true, + "job": true, + "namespace 2": true }, "indexByName": { - "namespace": 0, - "subscribers": 2, - "track": 1 + "bitrate": 6, + "codec": 3, + "data_tx": 7, + "fps": 5, + "namespace": 1, + "peak": 9, + "qoe": 11, + "qos": 10, + "resolution": 4, + "status": 12, + "subs": 8, + "track": 0, + "type": 2 }, "renameByName": { - "Value": "subscribers" + "Value #A": "Subs", + "Value #B": "Peak", + "bitrate": "Bitrate", + "codec": "Codec", + "data_tx": "Data Tx", + "fps": "FPS", + "namespace": "Origin", + "namespace 1": "Origin", + "qoe": "QoE", + "qos": "QoS", + "resolution": "Resolution", + "status": "Status", + "track": "Stream", + "type": "Type" } } } From 50bb95ad54c551bccd55b5bf7e56be3eefe85598 Mon Sep 17 00:00:00 2001 From: Giovanni Marzot Date: Wed, 29 Jul 2026 15:02:41 -0400 Subject: [PATCH 7/9] grafana: capture demo dashboard state; 10s build-info scrape Overview: echarts readouts for latency/loss/ingress/egress with logo-derived trace colours, native pill cells for QoS/QoE/Status, LIVE vs DONE computed from the time window, stable top-10 subscriptions graph, full-resolution peak queries, placeholder media attributes derived from track names. prometheus: scrape /info at 10s so the version badge tracks a redeploy. --- .../dashboards/moqx-overview.json | 742 +++++++++--------- docker/prometheus/prometheus.yml | 6 +- 2 files changed, 386 insertions(+), 362 deletions(-) diff --git a/docker/grafana/provisioning/dashboards/moqx-overview.json b/docker/grafana/provisioning/dashboards/moqx-overview.json index cd6ab2b85..b95a76226 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 version}}{{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-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: 20px; }\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}", + "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(group by (version) (last_over_time(moqx_build_info[30s])), \"instance\", \"moqx-host\", \"\", \"\")", "format": "table", "instant": true, "range": false, @@ -485,7 +485,7 @@ }, "gridPos": { "h": 4, - "w": 3, + "w": 2, "x": 0, "y": 6 }, @@ -514,7 +514,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 +525,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 +549,7 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Distinct namespaces with tracks visible in the relay /state. Peak is over the dashboard time range.", + "description": "Distinct (namespace, track) pairs visible in the relay /state. Peak is over the dashboard time range.", "fieldConfig": { "defaults": { "thresholds": { @@ -570,11 +570,11 @@ }, "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
", @@ -610,14 +610,14 @@ "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, "refId": "B" } ], - "title": "Namespaces", + "title": "NS", "transformations": [ { "id": "joinByField", @@ -655,8 +655,8 @@ }, "gridPos": { "h": 4, - "w": 3, - "x": 6, + "w": 2, + "x": 4, "y": 6 }, "id": 61, @@ -695,7 +695,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, @@ -740,11 +740,11 @@ }, "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
", @@ -780,14 +780,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 +804,7 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Aggregate active subscriptions (authoritative under load). Peak is over the dashboard time range.", + "description": "Active publishers connected to this relay. Peak is over the dashboard time range.", "fieldConfig": { "defaults": { "thresholds": { @@ -825,11 +825,11 @@ }, "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
", @@ -865,14 +865,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,37 +889,53 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "QUIC RTT: headline is current p50; p50 and p99 overlaid behind it.", - "fieldConfig": { - "defaults": { - "decimals": 1, - "mappings": [], - "unit": "ms" - }, - "overrides": [] - }, + "description": "QUIC RTT: current p50 readout, with p50 and p99 overlaid behind it.", "gridPos": { "h": 4, "w": 3, - "x": 15, + "x": 10, "y": 6 }, "id": 108, "options": { + "baidu": { + "callback": "bmapReady", + "key": "" + }, "editor": { "format": "auto", "height": 600 }, - "getOption": "\nconst frames = context.panel.data.series || [];\nconst INK = '#e6edf3', MUTED = 'rgba(230,237,243,.45)';\nconst C = { p50: '#22b8cf', p99: '#7ae0ef' };\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\nfunction line(data, color, width, fill) {\n return {\n type: 'line', data, smooth: 0.4, 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 dot = (x, color) => ({\n type: 'circle', right: x, top: 8, shape: { cx: 0, cy: 0, r: 3 },\n style: { fill: color },\n});\nconst tag = (x, text) => ({\n type: 'text', right: x, top: 3,\n style: { text, fill: MUTED, font: '10px monospace' },\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: 10, top: 26,\n style: { text: fmt(last(p50)), fill: INK,\n font: '600 28px Inter, system-ui, sans-serif' } },\n { type: 'text', left: 10, top: 26,\n style: { text: '', fill: MUTED } },\n { type: 'text', left: 10 + String(fmt(last(p50))).length * 16, top: 38,\n style: { text: 'ms', fill: MUTED, font: '11px monospace' } },\n dot(74, C.p50), tag(46, 'p50'),\n dot(30, C.p99), tag(4, 'p99'),\n ],\n};\n", - "renderer": "canvas" + "editorMode": "code", + "gaode": { + "key": "", + "plugin": "AMap.Scale,AMap.ToolBar" + }, + "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.25, 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.p50), keyLabel(2, 'p50'),\n keyDot(15, C.p99), keyLabel(15, 'p99'),\n ],\n};\n", + "google": { + "callback": "gmapReady", + "key": "" + }, + "map": "none", + "renderer": "canvas", + "themeEditor": { + "config": "{}", + "name": "default" + }, + "visualEditor": { + "code": "return {\n dataset: context.editor.dataset,\n series: context.editor.series,\n xAxis: {\n type: 'time',\n },\n yAxis: {\n type: 'value',\n min: 'dataMin',\n },\n}\n", + "dataset": [], + "series": [] + } }, + "pluginVersion": "7.2.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "expr": "histogram_quantile(0.50, sum by (le) (rate(moqx_quicRttSample_milliseconds_bucket[2m]))) >= 0 or vector(0)", + "expr": "histogram_quantile(0.50, sum by (le) (rate(moqx_quicRttSample_milliseconds_bucket[15s]))) >= 0 or vector(0)", "legendFormat": "p50", "refId": "A" }, @@ -928,7 +944,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "histogram_quantile(0.99, sum by (le) (rate(moqx_quicRttSample_milliseconds_bucket[2m]))) >= 0 or vector(0)", + "expr": "histogram_quantile(0.99, sum by (le) (rate(moqx_quicRttSample_milliseconds_bucket[15s]))) >= 0 or vector(0)", "legendFormat": "p99", "refId": "B" } @@ -941,307 +957,137 @@ "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": "#8b7bea", - "mode": "fixed" - }, "decimals": 1, "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "#8b7bea", - "value": null - } - ] - }, - "unit": "pps" + "unit": "percent" }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "loss" - }, - "properties": [ - { - "id": "unit", - "value": "percent" - }, - { - "id": "decimals", - "value": 2 - }, - { - "id": "color", - "value": { - "fixedColor": "#8b7bea", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "drops" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "#c3b5f5", - "mode": "fixed" - } - } - ] - } - ] + "overrides": [] }, "gridPos": { "h": 4, "w": 3, - "x": 18, + "x": 13, "y": 6 }, "id": 116, "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 + "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.25, 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", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "expr": "100 * sum(rate(moqx_quicPacketLoss_total[2m])) / clamp_min(sum(rate(moqx_quicPacketsSent_total[2m])), 1)", + "expr": "100 * (sum(rate(moqx_quicPacketLoss_total[15s])) + sum(rate(moqx_quicDatagramsDroppedOnRead_total[15s])) + sum(rate(moqx_quicDatagramsDroppedOnWrite_total[15s]))) / clamp_min(sum(rate(moqx_quicPacketsSent_total[15s])) + sum(rate(moqx_quicPacketsReceived_total[15s])), 1)", "legendFormat": "loss", "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "expr": "sum(rate(moqx_quicDatagramsDroppedOnRead_total[2m])) + sum(rate(moqx_quicDatagramsDroppedOnWrite_total[2m]))", - "legendFormat": "drops", - "refId": "B" } ], - "title": "Loss / drops", - "type": "stat" + "title": "Loss", + "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": "#1baf7a", - "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, "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 + "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.25, 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", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "expr": "rate(moqx_quicBytesRead_total[2m]) * 8", - "legendFormat": "in", + "expr": "rate(moqx_quicBytesRead_total[15s]) * 8", "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "expr": "rate(moqx_quicBytesWritten_total[2m]) * 8", - "legendFormat": "out", - "refId": "B" } ], - "title": "BW in / out", - "type": "stat" + "title": "Ingress BW", + "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": 6, - "x": 0, - "y": 10 + "h": 4, + "w": 4, + "x": 20, + "y": 6 }, - "id": 23, + "id": 115, "options": { - "cellHeight": "sm", - "showHeader": true, - "sortBy": [] + "editor": { + "format": "auto", + "height": 600 + }, + "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.25, 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", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "expr": "moqx_relay_active_sessions", - "format": "table", - "instant": true, - "range": false, + "expr": "rate(moqx_quicBytesWritten_total[15s]) * 8", "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", + "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" }, @@ -1249,6 +1095,7 @@ "footer": { "reducers": [] }, + "headerAlign": "center", "inspect": false }, "mappings": [], @@ -1262,12 +1109,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": 9, - "x": 6, + "w": 10, + "x": 0, "y": 10 }, "id": 106, @@ -1288,7 +1192,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, @@ -1299,11 +1203,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", @@ -1321,14 +1236,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" @@ -1338,21 +1279,19 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Per-track media view. Columns without a metric behind them yet (type, codec, resolution, fps, bitrate, data tx) render as \u2014 and are wired as constant labels; QoS/QoE/status are placeholder pills.", + "description": "Per-track media view. Type/Codec/Resolution/FPS/Bitrate and the QoS/QoE pills are PLACEHOLDER values derived from the track name \u2014 no relay metric backs them yet. Subs, Peak and Status are real.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { - "align": "auto", + "align": "center", "cellOptions": { "type": "auto" }, "filterable": false, - "footer": { - "reducers": [] - }, + "headerAlign": "center", "inspect": false }, "mappings": [], @@ -1370,23 +1309,136 @@ { "matcher": { "id": "byName", - "options": "QoS" + "options": "Name" }, "properties": [ { - "id": "custom.cellOptions", - "value": { - "mode": "basic", - "type": "color-background" - } + "id": "custom.width", + "value": 190 }, + { + "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}" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Type" + }, + "properties": [ + { + "id": "custom.width", + "value": 70 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Codec" + }, + "properties": [ + { + "id": "custom.width", + "value": 96 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Resolution" + }, + "properties": [ + { + "id": "custom.width", + "value": 96 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "FPS" + }, + "properties": [ + { + "id": "custom.width", + "value": 62 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Bitrate" + }, + "properties": [ + { + "id": "custom.width", + "value": 84 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Subs" + }, + "properties": [ + { + "id": "custom.width", + "value": 70 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Peak" + }, + "properties": [ + { + "id": "custom.width", + "value": 70 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "QoS" + }, + "properties": [ { "id": "custom.align", "value": "center" }, { "id": "custom.width", - "value": 92 + "value": 86 + }, + { + "id": "custom.cellOptions", + "value": { + "colorMode": "mapped", + "type": "pill" + } }, { "id": "mappings", @@ -1398,7 +1450,7 @@ "index": 1 }, "FAILING": { - "color": "#e66767", + "color": "#e0453c", "index": 2 }, "NORMAL": { @@ -1418,20 +1470,20 @@ "options": "QoE" }, "properties": [ - { - "id": "custom.cellOptions", - "value": { - "mode": "basic", - "type": "color-background" - } - }, { "id": "custom.align", "value": "center" }, { "id": "custom.width", - "value": 92 + "value": 86 + }, + { + "id": "custom.cellOptions", + "value": { + "colorMode": "mapped", + "type": "pill" + } }, { "id": "mappings", @@ -1443,7 +1495,7 @@ "index": 1 }, "FAILING": { - "color": "#e66767", + "color": "#e0453c", "index": 2 }, "NORMAL": { @@ -1463,20 +1515,20 @@ "options": "Status" }, "properties": [ - { - "id": "custom.cellOptions", - "value": { - "mode": "basic", - "type": "color-background" - } - }, { "id": "custom.align", "value": "center" }, { "id": "custom.width", - "value": 92 + "value": 84 + }, + { + "id": "custom.cellOptions", + "value": { + "colorMode": "mapped", + "type": "pill" + } }, { "id": "mappings", @@ -1484,8 +1536,8 @@ { "options": { "DONE": { - "color": "#3987e5", - "index": 2 + "color": "#6e7680", + "index": 1 }, "LIVE": { "color": "#1baf7a", @@ -1493,7 +1545,7 @@ }, "OFFLINE": { "color": "#6e7680", - "index": 1 + "index": 2 } }, "type": "value" @@ -1501,57 +1553,13 @@ ] } ] - }, - { - "matcher": { - "id": "byName", - "options": "Stream" - }, - "properties": [ - { - "id": "custom.width", - "value": 190 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Subs" - }, - "properties": [ - { - "id": "custom.width", - "value": 70 - }, - { - "id": "custom.align", - "value": "right" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Peak" - }, - "properties": [ - { - "id": "custom.width", - "value": 70 - }, - { - "id": "custom.align", - "value": "right" - } - ] } ] }, "gridPos": { "h": 8, - "w": 9, - "x": 15, + "w": 14, + "x": 10, "y": 10 }, "id": 24, @@ -1572,7 +1580,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace(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\", \"\", \"\"), \"data_tx\", \"\u2014\", \"\", \"\"), \"qos\", \"NORMAL\", \"\", \"\"), \"qoe\", \"NORMAL\", \"\", \"\"), \"status\", \"LIVE\", \"\", \"\")", + "expr": "label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(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\", \"\", \"\"), \"type\", \"CMAF\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"codec\", \"H.264\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1920x1080\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"fps\", \"30\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"bitrate\", \"4.5 Mb/s\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1280x720\", \"track\", \"(?i).*cam-b.*\"), \"bitrate\", \"2.1 Mb/s\", \"track\", \"(?i).*cam-b.*\"), \"resolution\", \"3840x2160\", \"track\", \"(?i).*cam-c.*\"), \"fps\", \"60\", \"track\", \"(?i).*cam-c.*\"), \"bitrate\", \"18 Mb/s\", \"track\", \"(?i).*cam-c.*\"), \"codec\", \"H.265\", \"track\", \"(?i).*cam-c.*\"), \"type\", \"LOC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"codec\", \"AAC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"bitrate\", \"128 kb/s\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"type\", \"LOC\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"codec\", \"JSON\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"bitrate\", \"8 kb/s\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"qos\", \"NORMAL\", \"\", \"\"), \"qoe\", \"NORMAL\", \"\", \"\"), \"status\", \"LIVE\", \"\", \"\") or label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(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\", \"\", \"\"), \"type\", \"CMAF\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"codec\", \"H.264\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1920x1080\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"fps\", \"30\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"bitrate\", \"4.5 Mb/s\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1280x720\", \"track\", \"(?i).*cam-b.*\"), \"bitrate\", \"2.1 Mb/s\", \"track\", \"(?i).*cam-b.*\"), \"resolution\", \"3840x2160\", \"track\", \"(?i).*cam-c.*\"), \"fps\", \"60\", \"track\", \"(?i).*cam-c.*\"), \"bitrate\", \"18 Mb/s\", \"track\", \"(?i).*cam-c.*\"), \"codec\", \"H.265\", \"track\", \"(?i).*cam-c.*\"), \"type\", \"LOC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"codec\", \"AAC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"bitrate\", \"128 kb/s\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"type\", \"LOC\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"codec\", \"JSON\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"bitrate\", \"8 kb/s\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"qos\", \"NORMAL\", \"\", \"\"), \"qoe\", \"NORMAL\", \"\", \"\"), \"status\", \"DONE\", \"\", \"\") or label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(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\", \"\", \"\"), \"type\", \"CMAF\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"codec\", \"H.264\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1920x1080\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"fps\", \"30\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"bitrate\", \"4.5 Mb/s\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1280x720\", \"track\", \"(?i).*cam-b.*\"), \"bitrate\", \"2.1 Mb/s\", \"track\", \"(?i).*cam-b.*\"), \"resolution\", \"3840x2160\", \"track\", \"(?i).*cam-c.*\"), \"fps\", \"60\", \"track\", \"(?i).*cam-c.*\"), \"bitrate\", \"18 Mb/s\", \"track\", \"(?i).*cam-c.*\"), \"codec\", \"H.265\", \"track\", \"(?i).*cam-c.*\"), \"type\", \"LOC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"codec\", \"AAC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"bitrate\", \"128 kb/s\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"type\", \"LOC\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"codec\", \"JSON\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"bitrate\", \"8 kb/s\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"qos\", \"\u2014\", \"\", \"\"), \"qoe\", \"\u2014\", \"\", \"\"), \"status\", \"\u2014\", \"\", \"\"), \"namespace\", \"\u2014\", \"\", \"\"), \"track\", \"\u2014\", \"\", \"\")", "format": "table", "instant": true, "range": false, @@ -1583,7 +1591,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "max_over_time((max by (namespace, track) (moqx_track_subscribers))[$__range:30s])", + "expr": "max by (namespace, track) (max_over_time(moqx_track_subscribers[$__range]))", "format": "table", "instant": true, "range": false, @@ -1607,42 +1615,58 @@ "Time 1": true, "Time 2": true, "__name__": true, + "data_tx": true, "instance": true, "job": true, + "namespace": true, + "namespace 1": true, "namespace 2": true }, "indexByName": { - "bitrate": 6, - "codec": 3, - "data_tx": 7, - "fps": 5, - "namespace": 1, - "peak": 9, - "qoe": 11, - "qos": 10, - "resolution": 4, - "status": 12, - "subs": 8, + "Value #A": 6, + "Value #B": 7, + "bitrate": 5, + "codec": 2, + "fps": 4, + "qoe": 9, + "qos": 8, + "resolution": 3, + "status": 10, "track": 0, - "type": 2 + "type": 1 }, "renameByName": { "Value #A": "Subs", "Value #B": "Peak", "bitrate": "Bitrate", "codec": "Codec", - "data_tx": "Data Tx", "fps": "FPS", - "namespace": "Origin", - "namespace 1": "Origin", "qoe": "QoE", "qos": "QoS", "resolution": "Resolution", "status": "Status", - "track": "Stream", + "track": "Name", "type": "Type" } } + }, + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "equal", + "options": { + "value": "\u2014" + } + }, + "fieldName": "Name" + } + ], + "match": "any", + "type": "exclude" + } } ], "type": "table" @@ -1652,7 +1676,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": { @@ -1741,7 +1765,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..6f7baa6e5 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: 10s params: module: [moqx_info] static_configs: From 53f6555fbf026beb0168407e7ba3869bd4ba8351 Mon Sep 17 00:00:00 2001 From: Giovanni Marzot Date: Fri, 31 Jul 2026 17:10:21 -0400 Subject: [PATCH 8/9] grafana: scrape-resolution sparklines, tighter track columns, stored plugin defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sparklines fetch one point per relay scrape with a 10s rate window and a near-straight curve; the earlier flattening came from the query step, not the window. Track columns sized to content, pill columns to the pill. Rebuilt echarts panels now store the plugin defaults the frontend fills in, so opening the dashboard no longer reports phantom unsaved changes. prometheus: /info at 30s — the payload is just the version. --- .../dashboards/moqx-overview.json | 168 ++++++++++++++---- docker/prometheus/prometheus.yml | 2 +- 2 files changed, 134 insertions(+), 36 deletions(-) diff --git a/docker/grafana/provisioning/dashboards/moqx-overview.json b/docker/grafana/provisioning/dashboards/moqx-overview.json index b95a76226..0b5dbfd57 100644 --- a/docker/grafana/provisioning/dashboards/moqx-overview.json +++ b/docker/grafana/provisioning/dashboards/moqx-overview.json @@ -890,6 +890,10 @@ "uid": "prometheus" }, "description": "QUIC RTT: current p50 readout, with p50 and p99 overlaid behind it.", + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, "gridPos": { "h": 4, "w": 3, @@ -897,6 +901,7 @@ "y": 6 }, "id": 108, + "maxDataPoints": 500, "options": { "baidu": { "callback": "bmapReady", @@ -911,7 +916,7 @@ "key": "", "plugin": "AMap.Scale,AMap.ToolBar" }, - "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.25, 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.p50), keyLabel(2, 'p50'),\n keyDot(15, C.p99), keyLabel(15, 'p99'),\n ],\n};\n", + "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.p50), keyLabel(2, 'p50'),\n keyDot(15, C.p99), keyLabel(15, 'p99'),\n ],\n};\n", "google": { "callback": "gmapReady", "key": "" @@ -926,6 +931,9 @@ "code": "return {\n dataset: context.editor.dataset,\n series: context.editor.series,\n xAxis: {\n type: 'time',\n },\n yAxis: {\n type: 'value',\n min: 'dataMin',\n },\n}\n", "dataset": [], "series": [] + }, + "visualEditorOptions": { + "code": "return {}" } }, "pluginVersion": "7.2.5", @@ -935,7 +943,8 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "histogram_quantile(0.50, sum by (le) (rate(moqx_quicRttSample_milliseconds_bucket[15s]))) >= 0 or vector(0)", + "expr": "histogram_quantile(0.50, sum by (le) (rate(moqx_quicRttSample_milliseconds_bucket[10s]))) >= 0 or vector(0)", + "interval": "3s", "legendFormat": "p50", "refId": "A" }, @@ -944,12 +953,14 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "histogram_quantile(0.99, sum by (le) (rate(moqx_quicRttSample_milliseconds_bucket[15s]))) >= 0 or vector(0)", + "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", + "transparent": false, "type": "volkovlabs-echarts-panel" }, { @@ -973,26 +984,55 @@ "y": 6 }, "id": 116, + "maxDataPoints": 500, "options": { + "baidu": { + "callback": "bmapReady", + "key": "" + }, "editor": { "format": "auto", "height": 600 }, - "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.25, 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" + "gaode": { + "key": "", + "plugin": "AMap.Scale,AMap.ToolBar" + }, + "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", + "google": { + "callback": "gmapReady", + "key": "" + }, + "map": "none", + "renderer": "canvas", + "themeEditor": { + "config": "{}", + "name": "default" + }, + "visualEditor": { + "code": "return {}", + "dataset": [], + "series": [] + }, + "visualEditorOptions": { + "code": "return {}" + } }, + "pluginVersion": "7.2.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "expr": "100 * (sum(rate(moqx_quicPacketLoss_total[15s])) + sum(rate(moqx_quicDatagramsDroppedOnRead_total[15s])) + sum(rate(moqx_quicDatagramsDroppedOnWrite_total[15s]))) / clamp_min(sum(rate(moqx_quicPacketsSent_total[15s])) + sum(rate(moqx_quicPacketsReceived_total[15s])), 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" } ], "title": "Loss", + "transparent": false, "type": "volkovlabs-echarts-panel" }, { @@ -1014,25 +1054,54 @@ "y": 6 }, "id": 119, + "maxDataPoints": 500, "options": { + "baidu": { + "callback": "bmapReady", + "key": "" + }, "editor": { "format": "auto", "height": 600 }, - "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.25, 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" + "gaode": { + "key": "", + "plugin": "AMap.Scale,AMap.ToolBar" + }, + "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", + "google": { + "callback": "gmapReady", + "key": "" + }, + "map": "none", + "renderer": "canvas", + "themeEditor": { + "config": "{}", + "name": "default" + }, + "visualEditor": { + "code": "return {}", + "dataset": [], + "series": [] + }, + "visualEditorOptions": { + "code": "return {}" + } }, + "pluginVersion": "7.2.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "expr": "rate(moqx_quicBytesRead_total[15s]) * 8", + "expr": "rate(moqx_quicBytesRead_total[10s]) * 8", + "interval": "3s", "refId": "A" } ], "title": "Ingress BW", + "transparent": false, "type": "volkovlabs-echarts-panel" }, { @@ -1054,25 +1123,54 @@ "y": 6 }, "id": 115, + "maxDataPoints": 500, "options": { + "baidu": { + "callback": "bmapReady", + "key": "" + }, "editor": { "format": "auto", "height": 600 }, - "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.25, 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" + "gaode": { + "key": "", + "plugin": "AMap.Scale,AMap.ToolBar" + }, + "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", + "google": { + "callback": "gmapReady", + "key": "" + }, + "map": "none", + "renderer": "canvas", + "themeEditor": { + "config": "{}", + "name": "default" + }, + "visualEditor": { + "code": "return {}", + "dataset": [], + "series": [] + }, + "visualEditorOptions": { + "code": "return {}" + } }, + "pluginVersion": "7.2.5", "targets": [ { "datasource": { "type": "prometheus", "uid": "prometheus" }, - "expr": "rate(moqx_quicBytesWritten_total[15s]) * 8", + "expr": "rate(moqx_quicBytesWritten_total[10s]) * 8", + "interval": "3s", "refId": "A" } ], "title": "Egress BW", + "transparent": false, "type": "volkovlabs-echarts-panel" }, { @@ -1312,10 +1410,6 @@ "options": "Name" }, "properties": [ - { - "id": "custom.width", - "value": 190 - }, { "id": "custom.align", "value": "left" @@ -1332,6 +1426,10 @@ "url": "/grafana/d/moqx-overview/moqx-relay-overview?var-track=${__data.fields.Name}" } ] + }, + { + "id": "custom.width", + "value": 150 } ] }, @@ -1343,7 +1441,7 @@ "properties": [ { "id": "custom.width", - "value": 70 + "value": 58 } ] }, @@ -1355,7 +1453,7 @@ "properties": [ { "id": "custom.width", - "value": 96 + "value": 72 } ] }, @@ -1367,7 +1465,7 @@ "properties": [ { "id": "custom.width", - "value": 96 + "value": 88 } ] }, @@ -1379,7 +1477,7 @@ "properties": [ { "id": "custom.width", - "value": 62 + "value": 48 } ] }, @@ -1391,7 +1489,7 @@ "properties": [ { "id": "custom.width", - "value": 84 + "value": 74 } ] }, @@ -1403,7 +1501,7 @@ "properties": [ { "id": "custom.width", - "value": 70 + "value": 54 } ] }, @@ -1415,7 +1513,7 @@ "properties": [ { "id": "custom.width", - "value": 70 + "value": 54 } ] }, @@ -1429,10 +1527,6 @@ "id": "custom.align", "value": "center" }, - { - "id": "custom.width", - "value": 86 - }, { "id": "custom.cellOptions", "value": { @@ -1461,6 +1555,10 @@ "type": "value" } ] + }, + { + "id": "custom.width", + "value": 64 } ] }, @@ -1474,10 +1572,6 @@ "id": "custom.align", "value": "center" }, - { - "id": "custom.width", - "value": 86 - }, { "id": "custom.cellOptions", "value": { @@ -1506,6 +1600,10 @@ "type": "value" } ] + }, + { + "id": "custom.width", + "value": 64 } ] }, @@ -1519,10 +1617,6 @@ "id": "custom.align", "value": "center" }, - { - "id": "custom.width", - "value": 84 - }, { "id": "custom.cellOptions", "value": { @@ -1551,6 +1645,10 @@ "type": "value" } ] + }, + { + "id": "custom.width", + "value": 68 } ] } diff --git a/docker/prometheus/prometheus.yml b/docker/prometheus/prometheus.yml index 6f7baa6e5..d3ee7283e 100644 --- a/docker/prometheus/prometheus.yml +++ b/docker/prometheus/prometheus.yml @@ -60,7 +60,7 @@ scrape_configs: # scraped often enough that a redeploy shows up promptly. - job_name: moqx-info metrics_path: /probe - scrape_interval: 10s + scrape_interval: 30s params: module: [moqx_info] static_configs: From 0900b8076ca2fcf1da2812d4e1086423765e0ab5 Mon Sep 17 00:00:00 2001 From: Giovanni Marzot Date: Sun, 2 Aug 2026 12:01:02 -0400 Subject: [PATCH 9/9] =?UTF-8?q?grafana:=20report=20=E2=80=94=20for=20track?= =?UTF-8?q?=20attributes=20we=20have=20no=20source=20for?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type/Codec/Resolution/FPS/Bitrate come from the catalog, which the relay is not necessarily authorised to read; QoS/QoE need client telemetry. The columns stay so the shape is right, but they no longer carry synthesised values. Subs, Peak and Status remain real. --- .../dashboards/moqx-overview.json | 138 +++--------------- 1 file changed, 18 insertions(+), 120 deletions(-) diff --git a/docker/grafana/provisioning/dashboards/moqx-overview.json b/docker/grafana/provisioning/dashboards/moqx-overview.json index 0b5dbfd57..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}}{{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
", + "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": { @@ -139,7 +139,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace(group by (version) (last_over_time(moqx_build_info[30s])), \"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": { @@ -492,7 +491,7 @@ "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": { @@ -549,7 +548,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": { @@ -577,7 +575,7 @@ "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": { @@ -617,7 +615,7 @@ "refId": "B" } ], - "title": "NS", + "title": "Namespaces", "transformations": [ { "id": "joinByField", @@ -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": { @@ -662,7 +659,7 @@ "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": { @@ -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": { @@ -747,7 +743,7 @@ "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": { @@ -804,7 +800,6 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Active publishers connected to this relay. Peak is over the dashboard time range.", "fieldConfig": { "defaults": { "thresholds": { @@ -832,7 +827,7 @@ "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": { @@ -903,38 +898,13 @@ "id": 108, "maxDataPoints": 500, "options": { - "baidu": { - "callback": "bmapReady", - "key": "" - }, "editor": { "format": "auto", "height": 600 }, "editorMode": "code", - "gaode": { - "key": "", - "plugin": "AMap.Scale,AMap.ToolBar" - }, - "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.p50), keyLabel(2, 'p50'),\n keyDot(15, C.p99), keyLabel(15, 'p99'),\n ],\n};\n", - "google": { - "callback": "gmapReady", - "key": "" - }, - "map": "none", - "renderer": "canvas", - "themeEditor": { - "config": "{}", - "name": "default" - }, - "visualEditor": { - "code": "return {\n dataset: context.editor.dataset,\n series: context.editor.series,\n xAxis: {\n type: 'time',\n },\n yAxis: {\n type: 'value',\n min: 'dataMin',\n },\n}\n", - "dataset": [], - "series": [] - }, - "visualEditorOptions": { - "code": "return {}" - } + "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": "7.2.5", "targets": [ @@ -986,37 +956,13 @@ "id": 116, "maxDataPoints": 500, "options": { - "baidu": { - "callback": "bmapReady", - "key": "" - }, "editor": { "format": "auto", "height": 600 }, - "gaode": { - "key": "", - "plugin": "AMap.Scale,AMap.ToolBar" - }, + "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", - "google": { - "callback": "gmapReady", - "key": "" - }, - "map": "none", - "renderer": "canvas", - "themeEditor": { - "config": "{}", - "name": "default" - }, - "visualEditor": { - "code": "return {}", - "dataset": [], - "series": [] - }, - "visualEditorOptions": { - "code": "return {}" - } + "renderer": "canvas" }, "pluginVersion": "7.2.5", "targets": [ @@ -1056,37 +1002,13 @@ "id": 119, "maxDataPoints": 500, "options": { - "baidu": { - "callback": "bmapReady", - "key": "" - }, "editor": { "format": "auto", "height": 600 }, - "gaode": { - "key": "", - "plugin": "AMap.Scale,AMap.ToolBar" - }, + "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", - "google": { - "callback": "gmapReady", - "key": "" - }, - "map": "none", - "renderer": "canvas", - "themeEditor": { - "config": "{}", - "name": "default" - }, - "visualEditor": { - "code": "return {}", - "dataset": [], - "series": [] - }, - "visualEditorOptions": { - "code": "return {}" - } + "renderer": "canvas" }, "pluginVersion": "7.2.5", "targets": [ @@ -1125,37 +1047,13 @@ "id": 115, "maxDataPoints": 500, "options": { - "baidu": { - "callback": "bmapReady", - "key": "" - }, "editor": { "format": "auto", "height": 600 }, - "gaode": { - "key": "", - "plugin": "AMap.Scale,AMap.ToolBar" - }, + "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", - "google": { - "callback": "gmapReady", - "key": "" - }, - "map": "none", - "renderer": "canvas", - "themeEditor": { - "config": "{}", - "name": "default" - }, - "visualEditor": { - "code": "return {}", - "dataset": [], - "series": [] - }, - "visualEditorOptions": { - "code": "return {}" - } + "renderer": "canvas" }, "pluginVersion": "7.2.5", "targets": [ @@ -1377,7 +1275,7 @@ "type": "prometheus", "uid": "prometheus" }, - "description": "Per-track media view. Type/Codec/Resolution/FPS/Bitrate and the QoS/QoE pills are PLACEHOLDER values derived from the track name \u2014 no relay metric backs them yet. Subs, Peak and Status are real.", + "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": { @@ -1678,7 +1576,7 @@ "type": "prometheus", "uid": "prometheus" }, - "expr": "label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(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\", \"\", \"\"), \"type\", \"CMAF\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"codec\", \"H.264\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1920x1080\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"fps\", \"30\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"bitrate\", \"4.5 Mb/s\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1280x720\", \"track\", \"(?i).*cam-b.*\"), \"bitrate\", \"2.1 Mb/s\", \"track\", \"(?i).*cam-b.*\"), \"resolution\", \"3840x2160\", \"track\", \"(?i).*cam-c.*\"), \"fps\", \"60\", \"track\", \"(?i).*cam-c.*\"), \"bitrate\", \"18 Mb/s\", \"track\", \"(?i).*cam-c.*\"), \"codec\", \"H.265\", \"track\", \"(?i).*cam-c.*\"), \"type\", \"LOC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"codec\", \"AAC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"bitrate\", \"128 kb/s\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"type\", \"LOC\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"codec\", \"JSON\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"bitrate\", \"8 kb/s\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"qos\", \"NORMAL\", \"\", \"\"), \"qoe\", \"NORMAL\", \"\", \"\"), \"status\", \"LIVE\", \"\", \"\") or label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(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\", \"\", \"\"), \"type\", \"CMAF\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"codec\", \"H.264\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1920x1080\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"fps\", \"30\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"bitrate\", \"4.5 Mb/s\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1280x720\", \"track\", \"(?i).*cam-b.*\"), \"bitrate\", \"2.1 Mb/s\", \"track\", \"(?i).*cam-b.*\"), \"resolution\", \"3840x2160\", \"track\", \"(?i).*cam-c.*\"), \"fps\", \"60\", \"track\", \"(?i).*cam-c.*\"), \"bitrate\", \"18 Mb/s\", \"track\", \"(?i).*cam-c.*\"), \"codec\", \"H.265\", \"track\", \"(?i).*cam-c.*\"), \"type\", \"LOC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"codec\", \"AAC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"bitrate\", \"128 kb/s\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"type\", \"LOC\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"codec\", \"JSON\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"bitrate\", \"8 kb/s\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"qos\", \"NORMAL\", \"\", \"\"), \"qoe\", \"NORMAL\", \"\", \"\"), \"status\", \"DONE\", \"\", \"\") or label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(label_replace(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\", \"\", \"\"), \"type\", \"CMAF\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"codec\", \"H.264\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1920x1080\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"fps\", \"30\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"bitrate\", \"4.5 Mb/s\", \"track\", \"(?i).*(cam|program|preview|video|screen).*\"), \"resolution\", \"1280x720\", \"track\", \"(?i).*cam-b.*\"), \"bitrate\", \"2.1 Mb/s\", \"track\", \"(?i).*cam-b.*\"), \"resolution\", \"3840x2160\", \"track\", \"(?i).*cam-c.*\"), \"fps\", \"60\", \"track\", \"(?i).*cam-c.*\"), \"bitrate\", \"18 Mb/s\", \"track\", \"(?i).*cam-c.*\"), \"codec\", \"H.265\", \"track\", \"(?i).*cam-c.*\"), \"type\", \"LOC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"codec\", \"AAC\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"bitrate\", \"128 kb/s\", \"track\", \"(?i).*(audio|music|mic|voice).*\"), \"type\", \"LOC\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"codec\", \"JSON\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"bitrate\", \"8 kb/s\", \"track\", \"(?i).*(event|telemetry|chat|meta|catalog|data).*\"), \"qos\", \"\u2014\", \"\", \"\"), \"qoe\", \"\u2014\", \"\", \"\"), \"status\", \"\u2014\", \"\", \"\"), \"namespace\", \"\u2014\", \"\", \"\"), \"track\", \"\u2014\", \"\", \"\")", + "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,