nix

configuration files that power my machines
Log | Files | Refs | README | LICENSE

h2o.jsonnet (23598B)


      1 // H2O exposes every metric as its own metric name with only a `version` label,
      2 // so metric families (status-errors.*, http2-errors.*, quic.num-frames-*, ...)
      3 // are grouped here with {__name__=~"..."} selectors + label_replace.
      4 
      5 local g = import 'github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet';
      6 
      7 local dashboard = g.dashboard;
      8 local var = dashboard.variable;
      9 local ts = g.panel.timeSeries;
     10 local stat = g.panel.stat;
     11 local gauge = g.panel.gauge;
     12 local barGauge = g.panel.barGauge;
     13 local row = g.panel.row;
     14 local prometheus = g.query.prometheus;
     15 
     16 // ---------------------------------------------------------------------------
     17 // Shared selectors
     18 // ---------------------------------------------------------------------------
     19 local dsRef = { type: 'prometheus', uid: '${datasource}' };
     20 local selInner = 'job=~"$job",instance=~"$instance"';
     21 local sel = '{%s}' % selInner;
     22 
     23 // rate() of a metric family by __name__ prefix, relabelled to a short legend.
     24 // `keep_metric_names` is required because rate() otherwise drops __name__,
     25 // collapsing every family member to the same labelset (a "duplicate output
     26 // timeseries" error) and leaving label_replace nothing to match on.
     27 local groupRate(prefix, label) =
     28   'label_replace(rate({__name__=~"%s.+",%s}[$__rate_interval]) keep_metric_names, "%s", "$1", "__name__", "%s(.+)")'
     29   % [prefix, selInner, label, prefix];
     30 
     31 // rate() of a single counter.
     32 local rate(metric) = 'rate(%s%s[$__rate_interval])' % [metric, sel];
     33 
     34 // ---------------------------------------------------------------------------
     35 // Threshold step sets
     36 // ---------------------------------------------------------------------------
     37 local greenOnly = [{ color: 'green', value: null }];
     38 local usedSteps = [
     39   { color: 'green', value: null },
     40   { color: 'yellow', value: 75 },
     41   { color: 'orange', value: 85 },
     42   { color: 'red', value: 90 },
     43 ];
     44 local errSteps = [
     45   { color: 'green', value: null },
     46   { color: 'yellow', value: 1 },
     47   { color: 'red', value: 10 },
     48 ];
     49 local upDownMap = [{
     50   type: 'value',
     51   options: {
     52     '0': { text: 'DOWN', color: 'red', index: 0 },
     53     '1': { text: 'UP', color: 'green', index: 1 },
     54   },
     55 }];
     56 
     57 // ---------------------------------------------------------------------------
     58 // Query helper
     59 // ---------------------------------------------------------------------------
     60 local prom(expr, legend=null, refId='A', instant=false) =
     61   prometheus.new('${datasource}', expr)
     62   + prometheus.withRefId(refId)
     63   + prometheus.withEditorMode('code')
     64   + (if legend != null then prometheus.withLegendFormat(legend) else {})
     65   + (
     66     if instant then
     67       prometheus.withInstant(true)
     68       + prometheus.withFormat('table')
     69       + prometheus.withRange(false)
     70     else prometheus.withRange(true)
     71   );
     72 
     73 // ---------------------------------------------------------------------------
     74 // Panel helpers
     75 // ---------------------------------------------------------------------------
     76 local tsPanel(title, gp, targets, unit='short', steps=greenOnly, min=null, max=null, decimals=null, desc=null, fill=10, stack='none', interp='smooth', threshStyle='off', calcs=['lastNotNull', 'max', 'mean']) =
     77   ts.new(title)
     78   + ts.panelOptions.withGridPos(gp.h, gp.w, gp.x, gp.y)
     79   + { datasource: dsRef }
     80   + ts.queryOptions.withTargets(targets)
     81   + ts.standardOptions.withUnit(unit)
     82   + ts.standardOptions.color.withMode('palette-classic')
     83   + ts.standardOptions.thresholds.withMode('absolute')
     84   + ts.standardOptions.thresholds.withSteps(steps)
     85   + ts.fieldConfig.defaults.custom.withDrawStyle('line')
     86   + ts.fieldConfig.defaults.custom.withFillOpacity(fill)
     87   + ts.fieldConfig.defaults.custom.withGradientMode('opacity')
     88   + ts.fieldConfig.defaults.custom.withLineWidth(2)
     89   + ts.fieldConfig.defaults.custom.withLineInterpolation(interp)
     90   + ts.fieldConfig.defaults.custom.withShowPoints('never')
     91   + ts.fieldConfig.defaults.custom.withSpanNulls(false)
     92   + ts.fieldConfig.defaults.custom.withStacking({ mode: stack, group: 'A' })
     93   + ts.fieldConfig.defaults.custom.thresholdsStyle.withMode(threshStyle)
     94   + ts.options.legend.withDisplayMode('table')
     95   + ts.options.legend.withPlacement('bottom')
     96   + ts.options.legend.withShowLegend(true)
     97   + ts.options.legend.withCalcs(calcs)
     98   + ts.options.tooltip.withMode('multi')
     99   + ts.options.tooltip.withSort('desc')
    100   + (if min != null then ts.standardOptions.withMin(min) else {})
    101   + (if max != null then ts.standardOptions.withMax(max) else {})
    102   + (if decimals != null then ts.standardOptions.withDecimals(decimals) else {})
    103   + (if desc != null then ts.panelOptions.withDescription(desc) else {});
    104 
    105 local statPanel(title, gp, targets, unit='short', mappings=[], steps=greenOnly, colorMode='value', textMode='auto', graphMode='area', decimals=null, calc='lastNotNull', desc=null) =
    106   stat.new(title)
    107   + stat.panelOptions.withGridPos(gp.h, gp.w, gp.x, gp.y)
    108   + { datasource: dsRef }
    109   + stat.queryOptions.withTargets(targets)
    110   + stat.standardOptions.withUnit(unit)
    111   + stat.standardOptions.withMappings(mappings)
    112   + stat.standardOptions.color.withMode('thresholds')
    113   + stat.standardOptions.thresholds.withMode('absolute')
    114   + stat.standardOptions.thresholds.withSteps(steps)
    115   + stat.options.withColorMode(colorMode)
    116   + stat.options.withGraphMode(graphMode)
    117   + stat.options.withTextMode(textMode)
    118   + stat.options.withJustifyMode('auto')
    119   + stat.options.withOrientation('auto')
    120   + stat.options.reduceOptions.withCalcs([calc])
    121   + stat.options.reduceOptions.withFields('')
    122   + stat.options.reduceOptions.withValues(false)
    123   + (if decimals != null then stat.standardOptions.withDecimals(decimals) else {})
    124   + (if desc != null then stat.panelOptions.withDescription(desc) else {});
    125 
    126 local gaugePanel(title, gp, targets, unit='percent', steps=usedSteps, min=0, max=100, desc=null) =
    127   gauge.new(title)
    128   + gauge.panelOptions.withGridPos(gp.h, gp.w, gp.x, gp.y)
    129   + { datasource: dsRef }
    130   + gauge.queryOptions.withTargets(targets)
    131   + gauge.standardOptions.withUnit(unit)
    132   + gauge.standardOptions.withMin(min)
    133   + gauge.standardOptions.withMax(max)
    134   + gauge.standardOptions.color.withMode('thresholds')
    135   + gauge.standardOptions.thresholds.withMode('absolute')
    136   + gauge.standardOptions.thresholds.withSteps(steps)
    137   + gauge.options.withShowThresholdLabels(false)
    138   + gauge.options.withShowThresholdMarkers(true)
    139   + gauge.options.withOrientation('auto')
    140   + gauge.options.reduceOptions.withCalcs(['lastNotNull'])
    141   + gauge.options.reduceOptions.withFields('')
    142   + gauge.options.reduceOptions.withValues(false)
    143   + (if desc != null then gauge.panelOptions.withDescription(desc) else {});
    144 
    145 local barGaugePanel(title, gp, targets, unit='short', colorMode='continuous-GrYlRd', steps=greenOnly, display='gradient', orientation='horizontal', decimals=null, desc=null) =
    146   barGauge.new(title)
    147   + barGauge.panelOptions.withGridPos(gp.h, gp.w, gp.x, gp.y)
    148   + { datasource: dsRef }
    149   + barGauge.queryOptions.withTargets(targets)
    150   + barGauge.standardOptions.withUnit(unit)
    151   + barGauge.standardOptions.color.withMode(colorMode)
    152   + barGauge.standardOptions.thresholds.withMode('absolute')
    153   + barGauge.standardOptions.thresholds.withSteps(steps)
    154   + barGauge.options.withDisplayMode(display)
    155   + barGauge.options.withOrientation(orientation)
    156   + barGauge.options.withShowUnfilled(true)
    157   + barGauge.options.withValueMode('color')
    158   + barGauge.options.reduceOptions.withCalcs(['lastNotNull'])
    159   + barGauge.options.reduceOptions.withFields('')
    160   + barGauge.options.reduceOptions.withValues(false)
    161   + (if decimals != null then barGauge.standardOptions.withDecimals(decimals) else {})
    162   + (if desc != null then barGauge.panelOptions.withDescription(desc) else {});
    163 
    164 local rowPanel(title, y) =
    165   row.new(title)
    166   + row.withGridPos(y)
    167   + row.withCollapsed(false);
    168 
    169 // percentile series for a latency phase (h2o_<phase>_time_{0,25,50,75,99}).
    170 local pctTargets(metric) = [
    171   prom('%s_0%s' % [metric, sel], legend='p0 (min)', refId='A'),
    172   prom('%s_25%s' % [metric, sel], legend='p25', refId='B'),
    173   prom('%s_50%s' % [metric, sel], legend='p50', refId='C'),
    174   prom('%s_75%s' % [metric, sel], legend='p75', refId='D'),
    175   prom('%s_99%s' % [metric, sel], legend='p99', refId='E'),
    176 ];
    177 
    178 // ---------------------------------------------------------------------------
    179 // Variables
    180 // ---------------------------------------------------------------------------
    181 local datasourceVar =
    182   var.datasource.new('datasource', 'prometheus')
    183   + var.datasource.generalOptions.withLabel('Data source')
    184   + var.datasource.withRegex('')
    185   + { current: { selected: false, text: 'VictoriaMetrics', value: 'DS_PROMETHEUS' } };
    186 
    187 local queryVar(name, label, expr, valueLabel) =
    188   var.query.new(name)
    189   + var.query.withDatasourceFromVariable(datasourceVar)
    190   + var.query.queryTypes.withLabelValues(valueLabel, expr)
    191   + var.query.generalOptions.withLabel(label)
    192   + var.query.selectionOptions.withMulti(true)
    193   + var.query.selectionOptions.withIncludeAll(true)
    194   + var.query.withRefresh('time')
    195   + var.query.withSort(1);
    196 
    197 local jobVar = queryVar('job', 'Job', 'h2o_uptime', 'job');
    198 local instanceVar = queryVar('instance', 'Instance', 'h2o_uptime{job=~"$job"}', 'instance');
    199 
    200 // ---------------------------------------------------------------------------
    201 // Panels
    202 // ---------------------------------------------------------------------------
    203 local panels = [
    204   //=== Overview ============================================================
    205   rowPanel('Overview', 0),
    206 
    207   statPanel('Version', { h: 4, w: 3, x: 0, y: 1 },
    208             [prom('h2o_uptime%s' % sel, legend='{{version}}')],
    209             unit='short', graphMode='none', textMode='name', colorMode='none',
    210             desc='Running H2O version.'),
    211 
    212   statPanel('Uptime', { h: 4, w: 3, x: 3, y: 1 },
    213             [prom('h2o_uptime%s' % sel)],
    214             unit='s', graphMode='none',
    215             desc='Seconds since the current worker process started.'),
    216 
    217   statPanel('Connections', { h: 4, w: 3, x: 6, y: 1 },
    218             [prom('h2o_connections%s' % sel)],
    219             unit='short'),
    220 
    221   statPanel('Sessions', { h: 4, w: 3, x: 9, y: 1 },
    222             [prom('h2o_num_sessions%s' % sel)],
    223             unit='short'),
    224 
    225   statPanel('Generation', { h: 4, w: 3, x: 12, y: 1 },
    226             [prom('h2o_generation%s' % sel)],
    227             unit='short', graphMode='none',
    228             desc='Configuration generation — increments on each graceful reload.'),
    229 
    230   statPanel('Worker Threads', { h: 4, w: 3, x: 15, y: 1 },
    231             [prom('h2o_worker_threads%s' % sel)],
    232             unit='short', graphMode='none'),
    233 
    234   statPanel('Listeners', { h: 4, w: 3, x: 18, y: 1 },
    235             [prom('h2o_listeners%s' % sel)],
    236             unit='short', graphMode='none'),
    237 
    238   gaugePanel('Connection Utilization', { h: 4, w: 3, x: 21, y: 1 },
    239              [prom('h2o_connections%s / h2o_max_connections%s * 100' % [sel, sel])],
    240              desc='Current connections as a percentage of max-connections.'),
    241 
    242   //=== Connections =========================================================
    243   rowPanel('Connections', 5),
    244 
    245   tsPanel('Connections & Sessions', { h: 8, w: 12, x: 0, y: 6 },
    246           [
    247             prom('h2o_connections%s' % sel, legend='connections', refId='A'),
    248             prom('h2o_num_sessions%s' % sel, legend='sessions', refId='B'),
    249             prom('h2o_connections_active%s' % sel, legend='active', refId='C'),
    250             prom('h2o_connections_idle%s' % sel, legend='idle', refId='D'),
    251           ],
    252           unit='short',
    253           desc='Live connection and session counts.'),
    254 
    255   tsPanel('Connection Churn', { h: 8, w: 12, x: 12, y: 6 },
    256           [
    257             prom(rate('h2o_connections_idle_closed'), legend='idle-closed', refId='A'),
    258             prom(rate('h2o_connections_shutdown'), legend='shutdown', refId='B'),
    259           ],
    260           unit='cps',
    261           desc='Rate of connections closed for being idle, and connections shut down.'),
    262 
    263   //=== Request Latency =====================================================
    264   rowPanel('Request Latency (per H2O status durations, ms)', 14),
    265 
    266   tsPanel('Request Total Time — percentiles', { h: 8, w: 12, x: 0, y: 15 },
    267           pctTargets('h2o_request_total_time'),
    268           unit='ms', threshStyle='off',
    269           desc='End-to-end request time distribution (p0/p25/p50/p75/p99).'),
    270 
    271   tsPanel('Duration — percentiles', { h: 8, w: 12, x: 12, y: 15 },
    272           pctTargets('h2o_duration'),
    273           unit='ms',
    274           desc='Connection duration distribution.'),
    275 
    276   tsPanel('Request Phases @ p50', { h: 8, w: 12, x: 0, y: 23 },
    277           [
    278             prom('h2o_connect_time_50%s' % sel, legend='connect', refId='A'),
    279             prom('h2o_header_time_50%s' % sel, legend='header', refId='B'),
    280             prom('h2o_body_time_50%s' % sel, legend='body', refId='C'),
    281             prom('h2o_process_time_50%s' % sel, legend='process', refId='D'),
    282             prom('h2o_response_time_50%s' % sel, legend='response', refId='E'),
    283             prom('h2o_request_total_time_50%s' % sel, legend='total', refId='F'),
    284           ],
    285           unit='ms',
    286           desc='Median latency contribution of each request phase.'),
    287 
    288   tsPanel('Request Phases @ p99', { h: 8, w: 12, x: 12, y: 23 },
    289           [
    290             prom('h2o_connect_time_99%s' % sel, legend='connect', refId='A'),
    291             prom('h2o_header_time_99%s' % sel, legend='header', refId='B'),
    292             prom('h2o_body_time_99%s' % sel, legend='body', refId='C'),
    293             prom('h2o_process_time_99%s' % sel, legend='process', refId='D'),
    294             prom('h2o_response_time_99%s' % sel, legend='response', refId='E'),
    295             prom('h2o_request_total_time_99%s' % sel, legend='total', refId='F'),
    296           ],
    297           unit='ms',
    298           desc='Tail latency contribution of each request phase.'),
    299 
    300   //=== HTTP Status & Errors ================================================
    301   rowPanel('HTTP Status & Errors', 31),
    302 
    303   tsPanel('HTTP Status Errors', { h: 8, w: 12, x: 0, y: 32 },
    304           [prom(groupRate('h2o_status_errors_', 'code'), legend='{{code}}')],
    305           unit='cps', stack='normal', fill=25,
    306           desc='Per-status-code error response rate (4xx / 5xx).'),
    307 
    308   tsPanel('Protocol Errors', { h: 8, w: 12, x: 12, y: 32 },
    309           [
    310             prom(groupRate('h2o_http2_errors_', 'kind'), legend='h2: {{kind}}', refId='A'),
    311             prom(groupRate('h2o_http1_errors_', 'kind'), legend='h1: {{kind}}', refId='B'),
    312           ],
    313           unit='cps',
    314           desc='HTTP/1 and HTTP/2 protocol error rates by kind.'),
    315 
    316   barGaugePanel('Status Errors — total', { h: 7, w: 12, x: 0, y: 40 },
    317                 [prom('label_replace({__name__=~"h2o_status_errors_.+",%s}, "code", "$1", "__name__", "h2o_status_errors_(.+)")' % selInner, legend='{{code}}')],
    318                 unit='short', display='gradient'),
    319 
    320   tsPanel('HTTP/2 Activity', { h: 7, w: 12, x: 12, y: 40 },
    321           [
    322             prom(rate('h2o_http2_read_closed'), legend='read-closed', refId='A'),
    323             prom(rate('h2o_http2_write_closed'), legend='write-closed', refId='B'),
    324             prom(rate('h2o_http2_idle_timeout'), legend='idle-timeout', refId='C'),
    325             prom(rate('h2o_http2_streaming_requests'), legend='streaming-requests', refId='D'),
    326           ],
    327           unit='cps',
    328           desc='HTTP/2 stream lifecycle events.'),
    329 
    330   //=== TLS / SSL ===========================================================
    331   rowPanel('TLS / SSL', 47),
    332 
    333   tsPanel('TLS Handshakes', { h: 7, w: 12, x: 0, y: 48 },
    334           [
    335             prom(rate('h2o_ssl_handshake_full'), legend='full', refId='A'),
    336             prom(rate('h2o_ssl_handshake_resume'), legend='resume', refId='B'),
    337           ],
    338           unit='ops',
    339           desc='Full vs. resumed TLS handshakes per second.'),
    340 
    341   tsPanel('ALPN Negotiation', { h: 7, w: 12, x: 12, y: 48 },
    342           [
    343             prom(rate('h2o_ssl_alpn_h1'), legend='http/1.1', refId='A'),
    344             prom(rate('h2o_ssl_alpn_h2'), legend='h2', refId='B'),
    345           ],
    346           unit='ops',
    347           desc='Negotiated ALPN protocol per second.'),
    348 
    349   gaugePanel('Handshake Resume Ratio', { h: 6, w: 6, x: 0, y: 55 },
    350              [prom('sum(%s) / clamp_min(sum(%s) + sum(%s), 1) * 100'
    351                    % [rate('h2o_ssl_handshake_resume'), rate('h2o_ssl_handshake_full'), rate('h2o_ssl_handshake_resume')])],
    352              steps=[{ color: 'red', value: null }, { color: 'yellow', value: 40 }, { color: 'green', value: 70 }],
    353              desc='Share of handshakes served from session resumption (higher is cheaper).'),
    354 
    355   statPanel('SSL Errors', { h: 6, w: 6, x: 6, y: 55 },
    356             [prom('h2o_ssl_errors%s' % sel)],
    357             unit='short', graphMode='area', steps=errSteps),
    358 
    359   statPanel('Avg Full Handshake', { h: 6, w: 6, x: 12, y: 55 },
    360             [prom('%s / clamp_min(%s, 1)' % [rate('h2o_ssl_handshake_accumulated_time_full'), rate('h2o_ssl_handshake_full')])],
    361             unit='µs', graphMode='area',
    362             desc='Mean wall time per full handshake (H2O reports accumulated time in microseconds).'),
    363 
    364   statPanel('Avg Resume Handshake', { h: 6, w: 6, x: 18, y: 55 },
    365             [prom('%s / clamp_min(%s, 1)' % [rate('h2o_ssl_handshake_accumulated_time_resume'), rate('h2o_ssl_handshake_resume')])],
    366             unit='µs', graphMode='area',
    367             desc='Mean wall time per resumed handshake.'),
    368 
    369   //=== QUIC / HTTP3 ========================================================
    370   rowPanel('QUIC / HTTP3', 61),
    371 
    372   tsPanel('QUIC Packets', { h: 8, w: 12, x: 0, y: 62 },
    373           [
    374             prom(rate('h2o_quic_num_packets_received'), legend='received', refId='A'),
    375             prom(rate('h2o_quic_num_packets_sent'), legend='sent', refId='B'),
    376             prom(rate('h2o_quic_num_packets_lost'), legend='lost', refId='C'),
    377             prom(rate('h2o_quic_num_packets_ack_received'), legend='ack-received', refId='D'),
    378             prom(rate('h2o_quic_packet_processed'), legend='processed', refId='E'),
    379           ],
    380           unit='pps',
    381           desc='QUIC packet throughput and loss.'),
    382 
    383   tsPanel('QUIC Bytes', { h: 8, w: 12, x: 12, y: 62 },
    384           [
    385             prom(rate('h2o_quic_num_bytes_received'), legend='received', refId='A'),
    386             prom(rate('h2o_quic_num_bytes_sent'), legend='sent', refId='B'),
    387             prom(rate('h2o_quic_num_bytes_lost'), legend='lost', refId='C'),
    388             prom(rate('h2o_quic_num_bytes_stream_data_sent'), legend='stream-data-sent', refId='D'),
    389           ],
    390           unit='Bps',
    391           desc='QUIC byte throughput.'),
    392 
    393   statPanel('QUIC Packet Loss', { h: 6, w: 6, x: 0, y: 70 },
    394             [prom('%s / clamp_min(%s, 1) * 100' % [rate('h2o_quic_num_packets_lost'), rate('h2o_quic_num_packets_sent')])],
    395             unit='percent', graphMode='area', steps=errSteps,
    396             desc='Lost packets as a fraction of sent.'),
    397 
    398   tsPanel('QUIC Paths', { h: 6, w: 9, x: 6, y: 70 },
    399           [prom(groupRate('h2o_quic_num_paths_', 'event'), legend='{{event}}')],
    400           unit='cps',
    401           desc='QUIC path validation / migration events.'),
    402 
    403   tsPanel('HTTP/3 Packet Forwarding', { h: 6, w: 9, x: 15, y: 70 },
    404           [
    405             prom(rate('h2o_http3_packet_forwarded'), legend='forwarded', refId='A'),
    406             prom(rate('h2o_http3_forwarded_packet_received'), legend='forwarded-received', refId='B'),
    407           ],
    408           unit='pps'),
    409 
    410   tsPanel('QUIC Frames Received', { h: 8, w: 12, x: 0, y: 76 },
    411           [prom(groupRate('h2o_quic_num_frames_received_', 'frame'), legend='{{frame}}')],
    412           unit='cps',
    413           desc='Inbound QUIC frames by type.'),
    414 
    415   tsPanel('QUIC Frames Sent', { h: 8, w: 12, x: 12, y: 76 },
    416           [prom(groupRate('h2o_quic_num_frames_sent_', 'frame'), legend='{{frame}}')],
    417           unit='cps',
    418           desc='Outbound QUIC frames by type.'),
    419 
    420   tsPanel('QUIC Recovery & Congestion', { h: 7, w: 12, x: 0, y: 84 },
    421           [
    422             prom(rate('h2o_quic_num_ptos'), legend='ptos', refId='A'),
    423             prom(rate('h2o_quic_num_handshake_timeouts'), legend='handshake-timeouts', refId='B'),
    424             prom(rate('h2o_quic_num_initial_handshake_exceeded'), legend='initial-handshake-exceeded', refId='C'),
    425             prom(rate('h2o_quic_num_paced'), legend='paced', refId='D'),
    426             prom(rate('h2o_quic_num_rapid_start'), legend='rapid-start', refId='E'),
    427             prom(rate('h2o_quic_num_respected_app_limited'), legend='respected-app-limited', refId='F'),
    428           ],
    429           unit='cps',
    430           desc='QUIC loss recovery, pacing and congestion-control signals.'),
    431 
    432   tsPanel('QUIC ECN', { h: 7, w: 12, x: 12, y: 84 },
    433           [
    434             prom(rate('h2o_quic_num_packets_received_ecn_ect0'), legend='rx ect0', refId='A'),
    435             prom(rate('h2o_quic_num_packets_received_ecn_ect1'), legend='rx ect1', refId='B'),
    436             prom(rate('h2o_quic_num_packets_received_ecn_ce'), legend='rx ce', refId='C'),
    437             prom(rate('h2o_quic_num_packets_acked_ecn_ect0'), legend='acked ect0', refId='D'),
    438             prom(rate('h2o_quic_num_packets_acked_ecn_ect1'), legend='acked ect1', refId='E'),
    439             prom(rate('h2o_quic_num_packets_acked_ecn_ce'), legend='acked ce', refId='F'),
    440           ],
    441           unit='pps',
    442           desc='Explicit Congestion Notification marks on received and acked packets.'),
    443 
    444   //=== Memory & Internals ==================================================
    445   rowPanel('Memory & Internals', 91),
    446 
    447   tsPanel('Memory Pool Chunks', { h: 7, w: 12, x: 0, y: 92 },
    448           [
    449             prom('h2o_memory_mem_pool_chunks%s' % sel, legend='mem-pool', refId='A'),
    450             prom('h2o_memory_socket_ssl_chunks%s' % sel, legend='socket-ssl', refId='B'),
    451             prom('h2o_memory_socket_zerocopy_chunks%s' % sel, legend='socket-zerocopy', refId='C'),
    452             prom('h2o_memory_socket_zerocopy_inflight%s' % sel, legend='zerocopy-inflight', refId='D'),
    453           ],
    454           unit='short',
    455           desc='Recycled buffer chunk counts per allocator.'),
    456 
    457   tsPanel('Event Loop Latency', { h: 7, w: 12, x: 12, y: 92 },
    458           [prom('h2o_evloop_latency_nanosec%s' % sel, legend='evloop')],
    459           unit='ns',
    460           desc='Event-loop latency (only populated when latency-optimization probing is enabled).'),
    461 
    462   statPanel('mmap Errors', { h: 5, w: 6, x: 0, y: 99 },
    463             [prom('h2o_memory_mmap_errors%s' % sel)],
    464             unit='short', graphMode='area', steps=errSteps),
    465 
    466   statPanel('h2olog Lost', { h: 5, w: 6, x: 6, y: 99 },
    467             [prom('h2o_h2olog_lost%s' % sel)],
    468             unit='short', graphMode='area', steps=errSteps,
    469             desc='h2olog events dropped because the ring buffer was full.'),
    470 
    471   statPanel('Max Connections', { h: 5, w: 6, x: 12, y: 99 },
    472             [prom('h2o_max_connections%s' % sel)],
    473             unit='short', graphMode='none', colorMode='none'),
    474 
    475   statPanel('Soft Limit Min Age', { h: 5, w: 6, x: 18, y: 99 },
    476             [prom('h2o_soft_connection_limit_min_age%s' % sel)],
    477             unit='s', graphMode='none', colorMode='none',
    478             desc='Minimum connection age before the soft connection limit may close it.'),
    479 ];
    480 
    481 // Assign stable, unique panel IDs.
    482 local panelsWithIds = std.mapWithIndex(function(i, p) p + { id: i + 1 }, panels);
    483 
    484 // ---------------------------------------------------------------------------
    485 // Dashboard
    486 // ---------------------------------------------------------------------------
    487 dashboard.new('H2O')
    488 + dashboard.withUid('h2o')
    489 + dashboard.withTags(['h2o', 'http', 'webserver'])
    490 + dashboard.withRefresh('30s')
    491 + dashboard.withTimezone('browser')
    492 + dashboard.withVariables([datasourceVar, jobVar, instanceVar])
    493 + dashboard.withPanels(panelsWithIds)
    494 + { graphTooltip: 1, time: { from: 'now-6h', to: 'now' } }