Skip to header Skip to main navigation Skip to main content Skip to footer

Search form

Site branding

Home
What's up, home?

Main navigation

  • Home
  • Story mode
  • About
  • Features
  • Hardware list
  • Videos
  • GitHub (opens in new tab)
  • Daily AI generated stories (opens in new tab)

Zabbix Summit keynote tracing, gradients on Zabbix - What's up, home? part 150

Breadcrumbs

Breadcrumb

  • Home
  • Zabbix Summit keynote tracing, gradients on Zabbix - What's up, home? part 150

Main page content

By whatsuphome | 9:43 PM EEST, Tue October 13, 2026

TL;DR

As Zabbix 8.0 got proper APM, I tried to trace Alexei's keynote on Zabbix Summit 2026. Also a tip how to get gradients or other CSS effects to Zabbix without installing any browser add ons or touching Zabbix codebase.
Zabbix Summit 2026 keynote -- What's up, home? part 150

Zabbix Summit 2026 is over — huge thanks to whole Zabbix team and all my regular summit friends. Once again it was a blast! This was my seventh summit, out of which fifth year as a speaker. I'll blog about my talk later, but here's now something else.

Tracing Zabbix Summit Keynote

A day before leaving towards Zabbix Summit I got an idea. Zabbix 8.0 got proper APM, so why not try to ‘trace’ Alexei's keynote and while doing so, see what happens if Zabbix encounters a trace that lasts for about 40 minutes.

Claude quickly spun up a local webpage for me. Yes, this is very lacking, but as this is not a serious thing at all, this was enough. Idea is that whenever something happens on stage – topic moves to next section, audience laughs, Alexei loses pointer – I click on relevant button and the timestamp for that will be recorded. In the end the results will be then saved to suitable format so it can be sent to Zabbix for tracing.

Keynote Tracer

 

Keynote tracer during recording

While still at home, my early tests looked promising. I got events happening on timeline, for example.

Tracing view showing events on timelineCloser look to timeline view

But see what happens with the “production” version that's 38 minutes long.

38 minutes but no events

There's nothing. I've tried to click around and figure what's going on, but truly the UI is not showing anything. The raw JSON file contains the events I did click during the talk, and my Clickhouse has those too. Zabbix APM code or UX visual issue with overly long traces? I'll poke some more and if I find out this is an actual bug and not just me being new with all this Zabbix APM stuff, I'll then file a Zabbix bug report. Oh well, it was a good try! 

Tip: Add gradients and other CSS effects to Zabbix

Also right before the summit I learned that it is actually possible to add all kinds of visual effects to Zabbix without a need to touch Zabbix codebase manually or even without any additional browser add-ons. Here's gradients on graphs and faint glow around widget corners.

Gradients on graphsFaint glow around widget corners

How? 

Through nginx substitution filter module. The idea is that nginx injects additional content to page mid-flight. As I don't totally do not want to break YOUR nginx just because you cannot live without trying this out, I'm not providing you any “Here's a ready config file for you” guide, but rather do it like this. Config snippets and code are courtesy of Opencode Big Pickle prompting:

Add new location blocks for additional CSS

Zabbix "glow" skin + visual extras.

# Both assets live in /etc/nginx, deliberately outside the Zabbix
# docroot, so a Zabbix upgrade cannot overwrite them.
# Bump ?v= below whenever either file changes.
location = /zabbix-glow.css {
    alias /etc/nginx/zabbix-glow.css;
    default_type text/css;
    add_header Cache-Control "public, max-age=600";
}

location = /zabbix-grad.js {
    alias /etc/nginx/zabbix-grad.js;
    default_type application/javascript;
    add_header Cache-Control "public, max-age=600";
}

Add inside location ~ [^/]\.php(/|$) {

	# Rewrite only the HTML emitted by PHP. sub_filter_types text/html
        # means chart*.php (SVG/PNG), the JSON-RPC API and the CSS/JS assets
    	# served from location / all pass through byte-for-byte untouched.
    	sub_filter_types text/html;
    	sub_filter_once on;
    	sub_filter "</head>" "<link rel=\"stylesheet\" type=\"text/css\" href=\"zabbix-glow.css?v=2\"><script defer src=\"zabbix-grad.js?v=2\"></script></head>";

zabbix-glow.css 

/*
Zabbix "glow" skin

  • Injected into every Zabbix page by nginx sub_filter.
    Config: /etc/nginx/sites-available/zabbix (location ~ .php$)
    Edit THIS file only to tweak the look -- no nginx reload needed,
    just hard-refresh in the browser. The ?v= in the injected
    is what defeats the browser cache; bump it when changing this file.
    
    Scoped to the dark themes via html[theme=...] so anyone on a light
    theme sees no change at all.
    
    Companion script: /etc/nginx/zabbix-grad.js (gradients + gauge zone).
    It publishes --zbx-glow-color onto each gauge; the rules below read it.
    */
    /* ==================================================================
    
    TIME-SERIES / GRAPH WIDGET BOXES
    The graph widgets (graph, svggraph) render server-side SVG, so the
    plot itself is styleable -- see the problem-band and hintbox rules
    further down. This block only dresses the widget container.
    ================================================================== */
    html[theme="dark-blue-theme"] .dashboard-widget-graph,
    html[theme="dark-blue-theme"] .dashboard-widget-svggraph,
    html[theme="dark-blue-theme"] .dashboard-widget-itemhistory,
    html[theme="dark-blue-theme"] .dashboard-widget-scatterplot,
    html[theme="dark-blue-theme"] .dashboard-widget-item {
    border-radius: 8px;
    box-shadow:
    0 0 0 1px rgba(61, 152, 255, 0.22),
    0 0 18px rgba(61, 152, 255, 0.26),
    inset 0 0 28px rgba(0, 58, 122, 0.40);
    }
    /* Softer, dimmer glow on the plain dark theme. */
    html[theme="dark-theme"] .dashboard-widget-graph,
    html[theme="dark-theme"] .dashboard-widget-svggraph,
    html[theme="dark-theme"] .dashboard-widget-itemhistory {
    border-radius: 8px;
    box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.10),
    0 0 16px rgba(0, 0, 0, 0.55);
    }
    /* ==================================================================
    1b. GAUGE -- threshold-aware glow
    zabbix-grad.js reads which threshold band the value currently sits
    in (using the widget's own threshold colours) and publishes it as
    --zbx-glow-color on the gauge root. Default is the neutral blue, so
    a gauge with no thresholds still looks right before JS runs.
    ================================================================== */
    html[theme="dark-blue-theme"] .svg-gauge {
    --zbx-glow-color: #3d98ff;
    }
    html[theme="dark-blue-theme"] .svg-gauge-value-arc-sector {
    filter: drop-shadow(0 0 4px var(--zbx-glow-color, #3d98ff));
    }
    html[theme="dark-blue-theme"] .svg-gauge-needle {
    filter: drop-shadow(0 0 3px var(--zbx-glow-color, #3d98ff));
    }
    html[theme="dark-blue-theme"] .svg-gauge-value {
    filter: drop-shadow(0 0 5px var(--zbx-glow-color, #3d98ff));
    }
    /* Whole-gauge halo, drawn around the widget rather than the geometry. */
    html[theme="dark-blue-theme"] .dashboard-widget-gauge {
    filter: drop-shadow(0 0 12px color-mix(in srgb, var(--zbx-glow-color, #3d98ff) 45%, transparent));
    }
    /* ==================================================================
    1c. PIE CHART -- also SVG, so the arcs glow directly.
    ================================================================== */
    html[theme="dark-blue-theme"] .svg-pie-chart-arc {
    filter: drop-shadow(0 0 4px rgba(61, 152, 255, 0.45));
    }
    /* ==================================================================
    3. PROBLEM SEVERITY TINTING
    CSeverityHelper::getStyle() puts one of these classes on the problem
    cell, and they are also used on severity badges throughout the UI.
    Zabbix only sets a flat background from them; this adds the outer
    glow so severity reads pre-attentively. Escalates as severity rises.
    Info / not-classified are deliberately left alone -- they are not
    problems worth shouting about.
    ================================================================== */
    html[theme="dark-blue-theme"] .warning-bg {
    box-shadow: 0 0 0 1px rgba(255, 200, 89, 0.35), 0 0 10px rgba(255, 200, 89, 0.25);
    }
    html[theme="dark-blue-theme"] .average-bg {
    box-shadow: 0 0 0 1px rgba(255, 160, 89, 0.45), 0 0 12px rgba(255, 160, 89, 0.32);
    }
    html[theme="dark-blue-theme"] .high-bg {
    box-shadow: 0 0 0 1px rgba(233, 118, 89, 0.55), 0 0 14px rgba(233, 118, 89, 0.40);
    }
    html[theme="dark-blue-theme"] .disaster-bg {
    box-shadow: 0 0 0 1px rgba(228, 89, 89, 0.70), 0 0 18px rgba(228, 89, 89, 0.55);
    }
    /* ==================================================================
    4. GRAPH PROBLEM BANDS
    CSvgGraphProblems draws each problem period as
    with fill #AA4455 at opacity
    0.1 -- nearly invisible on a dark background, which defeats the
    point of drawing it. Lift it and give the base handle a matching
    edge so "was there an incident here" is answerable at a glance.
    ================================================================== */
    html[theme="dark-blue-theme"] .svg-graph-problem-box {
    opacity: 0.24 !important;
    }
    html[theme="dark-blue-theme"] .svg-graph-problem-handle {
    filter: drop-shadow(0 0 4px rgba(233, 118, 89, 0.75));
    }
    html[theme="dark-blue-theme"] .svg-graph-problems line {
    stroke-opacity: 0.55;
    }
    /* ==================================================================
    5. SERVER-UNREACHABLE BANNER
    servercheck.js only toggles this element in/out of view (it writes
    inline display:flex / none itself, so display is deliberately left
    alone here -- overriding it would just fight the script). It only
    reveals the element when the check fails, so whenever this is on
    screen the server really is down.
    ================================================================== */
    html[theme="dark-blue-theme"] .msg-global-footer {
    min-height: 44px;
    padding: 10px 16px;
    margin: 0;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-align: center;
    color: #1a0000;
    background: #e45959;
    border: 0;
    box-shadow: 0 0 24px rgba(228, 89, 89, 0.65);
    animation: zbx-banner-pulse 1.6s ease-in-out infinite;
    }
    @keyframes zbx-banner-pulse {
    0%, 100% { box-shadow: 0 0 18px rgba(228, 89, 89, 0.50); }
    50% { box-shadow: 0 0 34px rgba(228, 89, 89, 0.90); }
    }
    /* ==================================================================
    6. SPARKLINES
    .svg-sparkline-area is an SVG whose fill/fill-opacity come
    from inline attributes (set by component.z-sparkline.js), so this
    is a container-level glow only; zabbix-grad.js turns the flat fill
    into a vertical gradient the same way it does for graph areas.
    ================================================================== */
    html[theme="dark-blue-theme"] .svg-sparkline {
    --zbx-glow-color: #3d98ff;
    }
    html[theme="dark-blue-theme"] .svg-sparkline-area {
    filter: drop-shadow(0 0 3px color-mix(in srgb, var(--zbx-glow-color, #3d98ff) 55%, transparent));
    }
    /* ==================================================================
    7. GRAPH HINTBOX / CROSSHAIR
    The hover readout and helper line. Pure presentation: lift it off
    the plot, sharpen the metric colour chips, make it readable
    against a busy graph background.
    ================================================================== */
    html[theme="dark-blue-theme"] .svg-graph-hintbox {
    background: rgba(0, 24, 51, 0.94);
    border: 1px solid rgba(61, 152, 255, 0.45);
    border-radius: 5px;
    box-shadow: 0 6px 22px rgba(0, 0, 0, 0.55), 0 0 12px rgba(61, 152, 255, 0.25);
    padding: 5px 8px;
    }
    html[theme="dark-blue-theme"] .svg-graph-hintbox-item-color,
    html[theme="dark-blue-theme"] .svg-graph-hintbox-trigger-color {
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.45);
    }
    /* Helper line: lift it clear of the data. */
    html[theme="dark-blue-theme"] .svg-graph-line-auxiliary {
    stroke-opacity: 0.9;
    }
    /* ==================================================================
    OPTIONAL: gentle breathing pulse on graph widgets. Disabled by
    default -- it draws the eye, which can be annoying on a wall-mounted
    NOC display. Uncomment to enable.
    ================================================================== /
    /
    @keyframes zbx-glow-pulse {
    0%, 100% { box-shadow: 0 0 0 1px rgba(61,152,255,0.22), 0 0 14px rgba(61,152,255,0.22), inset 0 0 24px rgba(0,58,122,0.40); }
    50% { box-shadow: 0 0 0 1px rgba(61,152,255,0.40), 0 0 26px rgba(61,152,255,0.45), inset 0 0 34px rgba(0,58,122,0.55); }
    }
    html[theme="dark-blue-theme"] .dashboard-widget-graph {
    animation: zbx-glow-pulse 6s ease-in-out infinite;
    }
    */
    /* ==================================================================
    Respect users who ask for less motion.
    ================================================================== */
    @media (prefers-reduced-motion: reduce) {
    html[theme="dark-blue-theme"] * {
    animation: none !important;
    transition: none !important;
    }
    }⏎

zabbix-grad.js

  • /*
    Zabbix skin script: gradients + threshold-aware gauge glow
    Injected alongside /etc/nginx/zabbix-glow.css by nginx sub_filter.
    
    Zabbix has NO gradient support of its own -- its "Fill" option is a
    flat fill-opacity, and there is no anywhere in the
    Zabbix source. The graph widgets render server-side SVG and the
    sparklines are d3-built SVG, so their area paths are real DOM nodes
    we can restyle:
    
    (graph, svggraph)
    (tophosts / item cards)
    
    CSS alone cannot do this, because fill:url(#id) requires a
    element to exist inside that particular . CSS
    cannot create elements, so we build the defs here instead.
    
    The second job is the gauge: it reads which of the widget's own
    threshold bands the current value sits in and republishes that
    band's colour as --zbx-glow-color, so the glow turns amber/red as
    the reading approaches a limit instead of staying blue.
    */
    (function () {
    if (window.__zbxGraphGradients) {
    return;
    }
    window.__zbxGraphGradients = true;
    var SVG_NS = 'http://www.w3.org/2000/svg';
    var PREFIX = 'zbx-grad-';
    /* ------------------------------------------------------------------
    Shared helpers
    ------------------------------------------------------------------ */
    // "rgb(61, 152, 255)" or "#3d98ff" -> [61, 152, 255]
    function toRgb(color) {
    var hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(color).trim());
     if (hex) {
     	var h = hex[1];
    
     	if (h.length === 3) {
     		h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
     	}
    
     	return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
     }
    
     var parts = String(color).match(/[\d.]+/g);
    
     return parts && parts.length >= 3 ? parts.slice(0, 3).map(Number) : [61, 152, 255];
    }
    function makeGradient(defs, id, rgb, topOpacity, bottomOpacity) {
    var gradient = document.createElementNS(SVG_NS, 'linearGradient');
     gradient.setAttribute('id', id);
     // Vertical fade: strongest at the top, gone at the axis.
     gradient.setAttribute('x1', '0');
     gradient.setAttribute('y1', '0');
     gradient.setAttribute('x2', '0');
     gradient.setAttribute('y2', '1');
    
     var top = document.createElementNS(SVG_NS, 'stop');
     top.setAttribute('offset', '0');
     top.setAttribute('stop-color', 'rgb(' + rgb.join(',') + ')');
     top.setAttribute('stop-opacity', String(topOpacity));
    
     var bottom = document.createElementNS(SVG_NS, 'stop');
     bottom.setAttribute('offset', '1');
     bottom.setAttribute('stop-color', 'rgb(' + rgb.join(',') + ')');
     bottom.setAttribute('stop-opacity', String(bottomOpacity));
    
     gradient.appendChild(top);
     gradient.appendChild(bottom);
     defs.appendChild(gradient);
    
     return gradient;
    }
    function ensureDefs(svg) {
    var defs = svg.querySelector(':scope > defs');
     if (!defs) {
     	defs = document.createElementNS(SVG_NS, 'defs');
     	// Must be the first child so the ids resolve in this <svg>.
     	svg.insertBefore(defs, svg.firstChild);
     }
    
     return defs;
    }
    /* ------------------------------------------------------------------
    Area gradients (graph + sparkline)
    ------------------------------------------------------------------ */
    /**
    @param {Element} svg the containing the paths
    @param {string} sel selector for the area paths
    @param {number} topOp stop-opacity at the top of the plot
    */
    function gradientise(svg, sel, topOp) {
    var areas = svg.querySelectorAll(sel);
    if (!areas.length) {
    return;
    }
    var defs = ensureDefs(svg);
    Array.prototype.forEach.call(areas, function (area, i) {
    var id = PREFIX + i;
     if (!svg.querySelector('#' + id)) {
     	// The metric colour lives in an inline <style> block (graph) or
     	// in a fill attribute (sparkline); getComputedStyle resolves both.
     	// Read it per area -- each dataset has its own colour.
     	makeGradient(defs, id, toRgb(getComputedStyle(area).fill), topOp, 0.02);
     }
    
     area.style.fill = 'url(#' + id + ')';
     // The gradient carries its own alpha, so the flat fill-opacity Zabbix
     // wrote inline has to go -- otherwise it multiplies down to invisible.
     area.style.fillOpacity = '1';
    });
    }
    /* ------------------------------------------------------------------
    1b. Threshold-aware gauge glow
    
    Geometry note: both the value arc and every threshold sector are
    built by the widget's #defineArc()/#polarToCartesian(), which is
    x = rcos(a), y = 1 + rsin(a). So the first coordinate pair of
    each path maps back to an angle by atan2(y - 1, x). Because we
    only ever compare two angles produced by that same function, no
    correction for the widget's own rotation is needed -- and we never
    depend on the needle, whose transform origin makes the frame
    ambiguous.
    ------------------------------------------------------------------ */
    function pathAngles(d) {
    if (!d) {
    return null;
    }
     // "M x1 y1 A r r 0 large sweep x2 y2 L ..."
     var m = /^\s*M\s+(-?[\d.eE+]+)\s+(-?[\d.eE+]+)\s+A\s+(-?[\d.eE+]+)\s+(-?[\d.eE+]+)\s+(-?[\d.eE+]+)\s+(\d)\s+([01])\s+(-?[\d.eE+]+)\s+(-?[\d.eE+]+)/.exec(d);
    
     if (!m) {
     	return null;
     }
    
     return [
     	Math.atan2(parseFloat(m[2]) - 1, parseFloat(m[1])) * 180 / Math.PI,
     	Math.atan2(parseFloat(m[9]) - 1, parseFloat(m[8])) * 180 / Math.PI
     ];
    }
    function gaugeZoneColour(gauge) {
    var valueArc = gauge.querySelector('.svg-gauge-value-arc-sector');
     if (!valueArc) {
     	return null;
     }
    
     var valueAngles = pathAngles(valueArc.getAttribute('d'));
    
     if (!valueAngles) {
     	return null;
     }
    
     // For the value arc, defineArc(-angle/2, value_angle) means the M
     // point sits at the value angle and the arc endpoint at the sweep
     // start -- so only the first angle is the reading itself.
     var valueAngle = valueAngles[0];
     var sectors = gauge.querySelectorAll('.svg-gauge-thresholds-arc-sector');
    
     if (!sectors.length) {
     	return null;
     }
    
     var fallback = null;
    
     for (var i = 0; i < sectors.length; i++) {
     	var range = pathAngles(sectors[i].getAttribute('d'));
    
     	if (!range) {
     		continue;
     	}
    
     	var lo = Math.min(range[0], range[1]);
     	var hi = Math.max(range[0], range[1]);
     	var fill = sectors[i].style && sectors[i].style.fill ? sectors[i].style.fill : null;
    
     	// Sectors tile the whole sweep; a band with no colour set means
     	// "below the first threshold", so remember the last coloured one.
     	if (fill) {
     		fallback = fill;
     	}
    
     	if (valueAngle >= lo && valueAngle <= hi) {
     		return fill || fallback;
     	}
     }
    
     return null;
    }
    function updateGauge(gauge) {
    var colour = gaugeZoneColour(gauge);
     if (colour) {
     	gauge.style.setProperty('--zbx-glow-color', colour);
     } else {
     	// No thresholds (or no value arc): fall back to the CSS default.
     	gauge.style.removeProperty('--zbx-glow-color');
     }
    }
    /* ------------------------------------------------------------------
    Scanning
    ------------------------------------------------------------------ */
    function scan() {
    Array.prototype.forEach.call(document.querySelectorAll('svg'), function (svg) {
    if (svg.querySelector('.svg-graph-area')) {
    gradientise(svg, '.svg-graph-area', 0.45);
    }
     	if (svg.querySelector('.svg-sparkline-area')) {
     		gradientise(svg, '.svg-sparkline-area', 0.55);
     	}
    
     	if (svg.querySelector('.svg-gauge-value-arc-sector')) {
     		updateGauge(svg);
     	}
     });
    
     // Gauges/sparklines sometimes sit in a <div> wrapper rather than a
     // <svg> root, so also catch any gauge root not visited above.
     Array.prototype.forEach.call(document.querySelectorAll('.svg-gauge, .svg-sparkline'), function (root) {
     	var svg = root.tagName.toLowerCase() === 'svg' ? root : root.querySelector('svg');
    
     	if (svg) {
     		if (root.classList.contains('svg-gauge')) {
     			updateGauge(svg);
     		}
     		if (root.classList.contains('svg-sparkline') && svg.querySelector('.svg-sparkline-area')) {
     			gradientise(svg, '.svg-sparkline-area', 0.55);
     		}
     	}
     });
    }
    var pending = null;
    function schedule() {
    if (pending) {
    return;
    }
     // Zabbix redraws on every data refresh and gauges animate their arc;
     // coalesce those bursts rather than re-scanning per mutation.
     pending = setTimeout(function () {
     	pending = null;
     	scan();
     }, 250);
    }
    if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', schedule);
    } else {
    schedule();
    }
    // Re-apply when a chart is redrawn (childList) or when an arc animates
    // (the gauge rewrites 'd' each frame).
    new MutationObserver(schedule).observe(document.documentElement, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['d']
    });
    })();⏎
    

 

 

whatsuphome
zabbixsummit2026
zabbix80
apm
visuals
nginx
  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
As Zabbix 8.0 got proper APM, I tried to trace Alexei's keynote on Zabbix Summit 2026. Also a tip how to get gradients or other CSS effects to Zabbix without installing any browser add ons or touching Zabbix codebase.

Search

Who am I?

  • Janne Pikkarainen from Finland 🇫🇮
  • Began with computers in 1986 with C64
  • Doing IT for living since 1999
  • Principal Site Reliability Engineer at Forcepoint
  • A Zabbix Summit speaker every year since 2022
  • Walking, talking Zabbix commercial

Support my project

Buy me a coffee 

Footer menu

  • Contact

What's up, home? | Janne Pikkarainen | contact(at)whatsuphome.fi | 2022 - 2026

Developed and Designed by Janne Pikkarainen