Skip to content

fix: log panel flickering in Grafana 12.3+ with newLogsPanel virtualization#1

Merged
hatemosphere merged 7 commits intomainfrom
fix/supplementary-queries-flicker
Mar 23, 2026
Merged

fix: log panel flickering in Grafana 12.3+ with newLogsPanel virtualization#1
hatemosphere merged 7 commits intomainfrom
fix/supplementary-queries-flicker

Conversation

@hatemosphere
Copy link
Copy Markdown
Member

@hatemosphere hatemosphere commented Mar 23, 2026

Summary

Fixes log panel flickering/row overlap in Grafana 12.3+ caused by three issues in how the plugin returns data frames to Grafana's new virtualized log panel (newLogsPanel, GA since 12.3).

Root cause

Grafana 12.3 introduced a virtualized log list that caches row heights by LogRowModel.uid. The uid is ${refId}_${idField.values[row]}. Without an id field, all rows shared uid A_null, causing the height cache to oscillate between different row heights in an infinite resetAfterIndex loop (13k+ calls/sec = visible flickering).

Additionally, sortPropNames ignored the shouldSortLogMessageField parameter, so logMessageField was sorted alphabetically instead of being placed first. Grafana picks the log body via getFirstFieldOfType(FieldType.string), so wrong field order = wrong body field = wrong height measurement.

Changes

pkg/quickwit/response_parser.go:

  • Add unique id field per log row (sequential index as fallback when doc has no id), matching Elasticsearch behavior where each hit has _id/id. Grafana's parseLegacyLogsFrame uses this for LogRowModel.uid, which the virtualizer uses as height cache key.
  • Fix sortPropNames to actually sort logMessageField to first position when shouldSortLogMessageField is true, matching the Grafana built-in Elasticsearch datasource implementation.

src/datasource/processResponse.ts:

  • Remove synthetic $qw_message field injection — now redundant since Go backend correctly sorts logMessageField first.
  • Fix data links array spread bug: [...(a || [], b)] (comma operator, drops existing links) → [...(a || []), ...b].

src/datasource/supplementaryQueries.ts:

  • Migrate from deprecated getDataProvider() to getSupplementaryRequest() API. The old code created an Observable that emitted {data: []} on every subscription. Matches how Grafana's built-in ES and Loki datasources work.

Test plan

  • Reproduced flickering locally with prod Quickwit data + Grafana 12.4.1
  • Confirmed ES datasource does NOT flicker with same data (0 resetAfterIndex calls vs 13k+/sec for QW)
  • Verified fix locally: unique id field → unique uids → no more infinite loop → no flickering
  • Verified message field correctly sorted to first position in frame

Upstream PR: quickwit-oss#174

hatemosphere and others added 7 commits March 23, 2026 09:30
…to fix log panel flickering

The plugin used the deprecated `getDataProvider()` API which created its own
Observable and immediately emitted `{ state: Loading, data: [] }` on every
subscription. This cleared log panel data before results arrived, causing
visible flickering/ghosting of log rows in Grafana Explore and dashboards.

Migrated to `getSupplementaryRequest()` which returns a plain DataQueryRequest,
letting Grafana's framework handle the Observable lifecycle. This matches how
Grafana's built-in Elasticsearch and Loki datasources work.

Also fixed a broken array spread in processResponse.ts where the comma operator
inside the spread caused data links to not be properly appended to existing ones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous approach created a synthetic `$qw_message` field and injected it
into the dataframe. This extra field caused Grafana's virtualized log list to
miscalculate row heights for multiline/wrapped log entries, resulting in rows
overlapping each other.

Now follows the same approach as Grafana's built-in Elasticsearch datasource:
reorder dataFrame.fields so the configured logMessageField is the first string
field. Grafana's parseLegacyLogsFrame picks the body via
getFirstFieldOfType(FieldType.string), so field ordering is sufficient.

For multiple comma-separated message fields, the first field's values are
replaced with concatenated content and extra source fields are removed, keeping
the field count unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Go backend's sortPropNames had the shouldSortLogMessageField parameter
but never used it, causing fields to be sorted purely alphabetically.
This meant the configured logMessageField (e.g. "message") ended up after
other string fields like "enr" alphabetically.

Grafana's parseLegacyLogsFrame picks the log body via
getFirstFieldOfType(FieldType.string), so when "enr" was first, it became
the body field. The virtualizer then measured "enr" values (short) for row
height instead of the actual "message" content (long), causing all rows to
get 50px height regardless of wrapped message length — resulting in rows
overlapping each other.

This matches the fix in Grafana's built-in Elasticsearch datasource
(response_utils.go) which correctly prepends logMessageField before the
alphabetical sort.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Go backend's sortPropNames now correctly sorts logMessageField to
first position, so the frontend reorderMessageField is unnecessary.
Removed it along with the original $qw_message synthetic field injection.

Kept: data links spread fix, data links application logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Grafana's virtualized log panel (newLogsPanel) uses _source for body text
measurement when calculating row heights with displayed fields. Without
_source, the virtualizer measures only the short message field value but
renders all displayed field values inline, causing height underestimation
and row overlap.

Serialize each document as a JSON string in _source, matching what the
built-in Elasticsearch datasource returns. This ensures the virtualizer
has the full document text for accurate height calculation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The _source field caused Grafana's handleOverflow/resetAfterIndex to
enter an infinite loop (13k+ calls/sec), making the panel flicker
continuously. The overflow correction never converges because _source
JSON length varies per row.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Grafana's virtualized log panel (newLogsPanel) uses LogRowModel.uid as
cache key for row height measurements. The uid is derived from the frame's
"id" field: uid = "${refId}_${idValue}". Without an "id" field, all rows
get uid "A_null" and share one cache entry.

When rows have different heights but share a cache key, the overflow
correction (handleOverflow → storeLogLineSize → resetAfterIndex) enters
an infinite loop: row 1 stores height 50, row 7 reads 50 but needs 660,
corrects to 660, row 1 reads 660 but needs 50, corrects to 50, repeat.
This causes 13k+ resetAfterIndex calls/sec = visible flickering.

The fix: generate a unique "id" field per row (matching ES behavior where
each hit has _id). Grafana's parseLegacyLogsFrame excludes the "id" field
from extraFields (line 49/63), so it won't appear as a displayed field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hatemosphere hatemosphere changed the title fix: replace deprecated getDataProvider to fix log panel flickering fix: log panel flickering in Grafana 12.3+ with newLogsPanel virtualization Mar 23, 2026
@hatemosphere hatemosphere merged commit 01baac1 into main Mar 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant