Skip to content

[receiver/mysqlreceiver] Add MySQL <8 and MariaDB multi-version support#47815

Open
cjksplunk wants to merge 59 commits intoopen-telemetry:mainfrom
cjksplunk:mysql-multi-version-support
Open

[receiver/mysqlreceiver] Add MySQL <8 and MariaDB multi-version support#47815
cjksplunk wants to merge 59 commits intoopen-telemetry:mainfrom
cjksplunk:mysql-multi-version-support

Conversation

@cjksplunk
Copy link
Copy Markdown
Contributor

Description

Extends the MySQL receiver to detect the database product and version at connect time and gate collection behavior accordingly, enabling use against MySQL 5.7.x and all supported MariaDB versions in addition to the existing MySQL 8+ support.

Version detection (fetchDBVersion) runs once at Connect() time via a single SELECT VERSION() query. Failure is non-fatal — the receiver falls back to MySQL <8 defaults and surfaces any connection error on the first scrape. The detected product (MySQL or MariaDB) and version (semver) are cached for the lifetime of the receiver instance.

Capability predicates gate three behaviors:

Predicate Minimum version Effect when false
supportsQuerySampleText() MySQL 8.0.3+ Uses topQueryNoSampleText.tmpl (no query_sample_text column); EXPLAIN is skipped
supportsProcesslist() MySQL 8.0.22+ client.port / network.peer.port remain 0; information_schema.PROCESSLIST is not used as fallback (global mutex, removed in MySQL 9.0)
supportsReplicaStatus() MySQL 8.0.22+ Falls back to SHOW SLAVE STATUS

Timer wait tiers (querySample.tmpl) — resolved in order:

  1. Exact TIMER_WAIT for completed waits (all versions)
  2. PS timer approximation for in-progress waits (MySQL 5.7+ / 8.x only — MariaDB's statement.TIMER_WAIT is updated only at yield points, not continuously)
  3. thread.processlist_time integer-second fallback (MySQL 5.7+, all MariaDB)

Scope attributesdb.version and db.product are stamped on the instrumentation scope of every emitted log record so consumers can identify the source product and version without parsing event attributes.

EOL warning — a startup log warning is emitted when a MySQL version past end-of-life is detected.

New file COMPATIBILITY.md documents every version-gated predicate, the timer wait tier logic, version detection semantics, and a tested-platforms matrix.

Minimum supported versions: MySQL 5.7.3+, 8.0+; MariaDB 10.5.2+, 11.x.

Link to tracking issue

Fixes #47302

Testing

  • Unit tests (client_test.go): TestDBVersionCapabilities covers all three predicates across MySQL 8.0.27, 8.0.22, 8.0.21, 8.0.3, 8.0.2, 8.0.0, 5.7.44, MariaDB 10.11.6, 11.4.2, and the zero value. TestParseDBVersion covers MySQL plain, MySQL with -log suffix, MariaDB, MariaDB with 5.5.5- compat prefix, and Debian-decorated MariaDB strings. TestGetDBVersionCaching and TestFetchDBVersionTimeout cover the one-shot caching and startup-stall safeguard.
  • Scraper unit tests (scraper_test.go): mock client extended with dbVersionOverride to simulate MySQL <8 and MariaDB; existing scraper tests updated to match new 14-field query sample fixture format.
  • Integration tests (integration_test.go): new TestMySQLMultiVersionCapabilities runs against Docker images for MySQL 8.4, MySQL 5.7, MariaDB 10.5, and MariaDB 11.4. Each container is started, scraped, and verified for the correct capability flags, EOL warnings, and log record content.
  • Live validation: tested against AWS RDS instances (MySQL 8.4.7, MySQL 5.7.44, MariaDB 10.5.28, MariaDB 11.8.2) — see COMPATIBILITY.md tested-platforms table.

Documentation

  • receiver/mysqlreceiver/README.md: updated Prerequisites section with a supported-versions table; added Query plan availability by version section; added events_waits_current enablement guide.
  • receiver/mysqlreceiver/COMPATIBILITY.md: new file — capability predicate reference, timer wait tier table, version detection semantics, tested-platforms matrix, events_waits_current consumer setup guide.

cjksplunk added 30 commits April 1, 2026 10:20
Detect the server version at runtime to handle MySQL <8 and all
MariaDB versions, which lack the query_sample_text column in
performance_schema.events_statements_summary_by_digest.

- Add dbProduct/dbVersion types with capability predicates
  (isMySQL8Plus, supportsQuerySampleText)
- Add getDBVersion() to the client interface with lazy caching
- Add topQueryNoSampleText.tmpl — 5-column fallback query template
- Branch getTopQueries() on supportsQuerySampleText() to select
  the appropriate template and scan set
- Generate explain plans for query samples; include result as the
  mysql.query_plan attribute on db.server.query_sample events
- Share a single plan cache between top-query and query-sample
  scrapers to avoid duplicate EXPLAIN calls
- Only create the shared plan cache when at least one log event
  scraper is enabled, preventing goroutine leaks in default config

Assisted-by: Claude Sonnet 4.6
…ties

- TestVersionCompatibility: verifies getDBVersion() correctly identifies
  MySQL vs MariaDB product and version flags (isMySQL8Plus,
  supportsQuerySampleText), and that getTopQueries() selects the correct
  6-column (MySQL 8+) or 5-column (MariaDB/<8) template by running
  without error against live containers

- TestIntegrationLogScraper: end-to-end proof that scrapeTopQueryFunc
  and scrapeQuerySampleFunc work on MySQL 8.0.33, MariaDB 10.11, and
  MariaDB 11.4; validates shared plan cache interaction and structural
  log record attributes

- Fix supportsQuerySampleText() boundary: QUERY_SAMPLE_TEXT was
  introduced in MySQL 8.0.3 RC, so the correct gate is isMySQL8Plus()
  (not 8.0.22); update client_test.go accordingly

Assisted-by: Claude Sonnet 4.6
- Use assert.NoError (not require) in defers so shutdown failures don't
  panic after the test has already stopped (M1)
- Log perf_schema setup errors in runPerfSchemaSetup instead of
  silently swallowing them (M2)
- Add sync comment to topQueryNoSampleText.tmpl pointing to base
  template to prevent silent divergence (M3)
- Introduce usesSampleText local in getTopQueries to avoid calling
  supportsQuerySampleText() twice (L6)

Assisted-by: Claude Sonnet 4.6
Replace the bare *dbVersion pointer cache with sync.Once + result
fields so concurrent calls to getDBVersion() are safe and SELECT
VERSION() executes exactly once per connection.

Also change the query() helper to take *sql.DB instead of mySQLClient
by value — mySQLClient is now non-copyable (contains sync.Once) and
query() only needed the sql.DB anyway.

Update TestGetDBVersionCaching to prime the Once directly rather than
setting the old cachedDBVersion field.

Assisted-by: Claude Sonnet 4.6
…ability by version

Assisted-by: Claude Sonnet 4.6
Adds connection-time version detection so the receiver selects the
correct query template and replication status command for each server:

- MySQL 8.0.22+: full 6-column top-query template (includes
  query_sample_text) and SHOW REPLICA STATUS
- MySQL <8 and all MariaDB versions: 5-column fallback template
  (no query_sample_text) and SHOW SLAVE STATUS

Key changes:
- client.go: fetchDBVersion() called during Connect(); getDBVersion()
  returns cached dbVersion; version detection failure is non-fatal
  (safe fallback used instead of erroring out)
- scraper.go: shared queryPlanCache passed through newMySQLScraper;
  scrapeTopQueries uses version-branched template; mysql.query_plan
  belongs only on db.server.top_query events, not query_sample events
- metadata.yaml: mysql.query_plan removed from db.server.query_sample;
  mysql.query_plan.hash retained on both event types
- templates/: topQuery.tmpl (MySQL 8+), topQueryNoSampleText.tmpl
  (MySQL <8/MariaDB), querySample.tmpl updated
- integration_test.go: TestIntegrationLogScraper and
  TestVersionCompatibility tests for MySQL 8, MariaDB 10.11, MariaDB 11.4
- scraper_test.go: TestScrapeQuerySamplesExplainPlan updated to reflect
  that explainQuery is not called from scrapeQuerySampleFunc
- expectedQuerySamples.yaml: removed stale mysql.query_plan attribute

Assisted-by: Claude Sonnet 4.6
- Fix nil-pointer panic in getReplicaStatusStats when fetchDBVersion
  fails at Connect time: add version == nil guard (same pattern as
  supportsQuerySampleText) so the safe SHOW SLAVE STATUS fallback is
  used when dbVersion is zero-valued
- Extract parseDBVersion from fetchDBVersion so version string parsing
  can be unit-tested without a live database; add TestParseDBVersion
  covering MySQL 8, MySQL 5.7/5.6, MySQL with suffix (-log), MariaDB
  10.x/11.x, and malformed input
- Rename TestSharedPlanCacheDeduplication to TestScrapeQuerySamplesNoExplain
  and fix misleading comment: scrapeQuerySampleFunc never calls
  explainQuery (explain runs only in the top-query scraper); the test
  was vacuously correct under the old name
- README: split MySQL 5.6 into its own row showing query_sample as
  unsupported — performance_schema.user_variables_by_thread (used by
  querySample.tmpl for traceparent) was introduced in MySQL 5.7.3

Assisted-by: Claude Sonnet 4.6
…ed version on start

Document that version detection in Connect() runs exactly once and is
non-fatal by design; a failure at startup means the receiver operates
with incorrect version information for its entire lifetime with no
retry. Log the detected product, version, and supports_query_sample_text
on successful detection, or warn when detection failed and the fallback
is active.

Assisted-by: Claude Sonnet 4.6
….3 and MariaDB <10.5.2

performance_schema.user_variables_by_thread is not available on MySQL
<5.7.3 or MariaDB <10.5.2. Add a fallback query sample template that
omits the join and returns an empty traceparent for those versions,
preventing unknown-table errors on every scrape. Add
supportsUserVariablesByThread() predicate to drive template selection
and include it in the startup version log.

Assisted-by: Claude Sonnet 4.6
…rtsUserVariablesByThread

Extend TestDBVersionCapabilities with wantSupportsUserVariablesByThread
covering MySQL 8, 5.7.44, 5.7.3 (boundary), 5.7.2 (below boundary),
5.6.51, MariaDB 10.11.6, 10.5.2 (boundary), 10.5.1 (below boundary),
11.4.2, and the zero-value case.

Add wantUserVarsByThread assertions to TestIntegrationLogScraper and
TestVersionCompatibility; add MySQL 5.7 case to TestIntegrationLogScraper
as the key boundary version. Add getQuerySamples no-error assertion to
TestVersionCompatibility to prove the correct template is selected per
server version.

Add EOL note to supportsUserVariablesByThread: MySQL 5.6 and 5.7 support
is included for completeness but both versions are past end-of-life.

Assisted-by: Claude Sonnet 4.6
Extract version-detection logging into logDetectedVersion() and add a
Warn log when MySQL <8 is detected, indicating the version is past
end-of-life and may not be supported in a future release. MariaDB
versions are not subject to this check.

Add TestLogDetectedVersion covering MySQL 8 (no warn), MySQL 5.7/5.6
(EOL warn), MariaDB 10.11/10.4 (no EOL warn), and unknown version
(fallback warn).

Assisted-by: Claude Sonnet 4.6
Set db.version and db.product on the instrumentation scope of all
emitted ScopeLogs at scrape time. These are derived from the version
detected at Connect and are available downstream via OTTL as
instrumentation_scope.attributes["db.version"] and
instrumentation_scope.attributes["db.product"].

Scope attributes are carried silently through the pipeline and ignored
by default — users who need them can access them without any data model
changes to resources or log records.

Assisted-by: Claude Sonnet 4.6
The scraper already caches detectedVersion after start(). Push all
version-based branching up to the scraper so the client interface
becomes a pure query executor:

- getTopQueries now takes supportsSampleText bool
- getQuerySamples now takes supportsUserVarsByThread bool
- getReplicaStatusStats now takes supportsReplicaStatus bool

Scraper call sites resolve these flags from m.detectedVersion using the
existing capability predicates. Add supportsReplicaStatus() predicate on
dbVersion to keep the pattern consistent with supportsQuerySampleText()
and supportsUserVariablesByThread().

Future metrics work that needs version-based decisions can use
m.detectedVersion directly without touching the client interface.

Assisted-by: Claude Sonnet 4.6
…lity refactor

- TestDBVersionCapabilities: add wantSupportsReplicaStatus field and
  assertions across all cases, including boundary versions 8.0.22
  (true) and 8.0.21 (false)
- TestReplicaStatusQuery: replace inlined version logic with
  supportsReplicaStatus() — tests now cover the actual predicate the
  scraper calls
- newTopQueryScraper: set s.detectedVersion from mc.getDBVersion() so
  that scraper-side capability flags match the mock version override;
  previously detectedVersion was zero value regardless of the mock

Assisted-by: Claude Sonnet 4.6
- Fix TestScrapeQuerySamplesExplainPlan and TestScrapeQuerySamplesNoExplain
  to set detectedVersion to MySQL 8 (previously ran with zero value)
- Remove redundant s.detectedVersion = v8 in TestScrapeTopQueryFuncScopeAttributes
  (now set by newTopQueryScraper via mc.getDBVersion())
- Add TestScrapeQuerySampleFuncFallbackVersion: exercises MySQL 5.6 path
  where supportsUserVariablesByThread() is false
- TestVersionCompatibility: add wantReplicaStatus field and call
  getReplicaStatusStats to verify SHOW REPLICA/SLAVE STATUS template selection
- TestIntegrationLogScraper: inject observer logger to assert logDetectedVersion
  EOL warn fires for MySQL 5.7 and not for MySQL 8/MariaDB; add scope attribute
  assertions (db.version, db.product) on both top-query and sample log output

Assisted-by: Claude Sonnet 4.6
… and changelog

Remove inaccurate claims that EXPLAIN/query plans are available on
db.server.query_sample events. Query plans are only emitted on
db.server.top_query events (MySQL 8.0+ only via query_sample_text).

Assisted-by: Claude Sonnet 4.6
…, and extend test coverage

- Extract `dbVersion.isValid()` and `dbVersion.productString()` to eliminate
  repeated nil checks and product string conversions across predicates and scraper
- Extract `emitLogsWithScopeAttrs()` to deduplicate the 3-line emit pattern
  shared by scrapeTopQueryFunc and scrapeQuerySampleFunc
- Hoist cache key construction in scrapeTopQueries to a single `cacheKey` var
- Replace inline scanRow conditional in getTopQueries with a closure defined
  once outside the loop
- Add TestDBVersionHelperMethods, TestScrapeQuerySampleFuncScopeAttributes, and
  TestScrapeTopQueryFuncScanRowWithSampleText to cover the refactored paths;
  extend mockClient to capture explainQuery call args

Assisted-by: Claude Sonnet 4.6
…k.peer.port on query samples

performance_schema.threads.processlist_host contains only the client hostname
or IP address — never a host:port pair. The original implementation called
net.SplitHostPort() on this value, which always failed for real MySQL/MariaDB
instances and logged a spurious error:

  "Failed to parse processlistHost value: missing port in address <ip>"

The test fixture worked around this by using a fabricated "192.168.1.80:1234"
value that no real MySQL instance would produce.

The fix queries the separate processlist_port column from
performance_schema.threads, which correctly provides the client port as an
independent integer value. Both templates (querySample.tmpl and
querySampleNoUserVars.tmpl) now select COALESCE(thread.processlist_port, 0),
the querySample struct gains a processlistPort field, and scraper.go uses both
fields directly without any parsing.

Assisted-by: Claude Sonnet 4.6
… port is unavailable

performance_schema.threads.processlist_host contains only the client hostname
or IP — never host:port. The previous commit attempted to query a separate
processlist_port column, but that column does not exist in any supported
MySQL or MariaDB version.

Per MySQL documentation (https://dev.mysql.com/doc/refman/8.0/en/performance-schema-threads-table.html
and the equivalent MySQL 5.7 page), the PROCESSLIST_HOST column deliberately
omits the port number for TCP/IP connections:

  "the PROCESSLIST_HOST column does not include the port number for TCP/IP
   connections. To obtain this information from the Performance Schema, enable
   the socket instrumentation... and examine the socket_instances table."

MariaDB's performance_schema.threads documentation
(https://mariadb.com/kb/en/performance-schema-threads-table/) likewise lists
no port column and makes no provision for obtaining it from this table.

Socket instrumentation (required to get the port via socket_instances) is
disabled by default and carries non-trivial overhead, making it unsuitable
as a general-purpose solution. client.port and network.peer.port are
therefore emitted as 0 (not populated).

The previous implementation called net.SplitHostPort() on processlist_host,
which always failed for real MySQL/MariaDB instances, producing a spurious
error log on every scrape interval. The test fixture had fabricated a
"192.168.1.80:1234" value that no real instance would produce.

Assisted-by: Claude Sonnet 4.6
…rmation_schema.PROCESSLIST

performance_schema.threads.processlist_host returns only a bare hostname/IP
with no port — this is deliberate per MySQL and MariaDB documentation. Port
information is not available in that table.

information_schema.PROCESSLIST.HOST returns "host:port" for TCP/IP connections
(e.g. "192.168.1.80:58061"), which is the correct source for both address and
port. The two tables are joined on PROCESSLIST_ID = ID.

The query sample templates now LEFT JOIN information_schema.PROCESSLIST and
select pl.HOST in place of thread.processlist_host. scraper.go parses the
result with net.SplitHostPort; Unix socket connections (no port) fall back
gracefully to address-only with port 0.

Assisted-by: Claude Sonnet 4.6
…esslist (MySQL 8.0.22+)

client.port and network.peer.port on db.server.query_sample events were always 0
because performance_schema.threads.PROCESSLIST_HOST is host-only by design.

performance_schema.processlist.HOST returns "host:port" for TCP/IP connections and
is available on MySQL 8.0.22+. A LEFT JOIN on PROCESSLIST_ID = ID is added to the
query sample templates when the supportsProcesslist() capability predicate is true.
SUBSTRING_INDEX splits host and port in SQL; the port is scanned into a new clientPort
field on the querySample struct and passed directly to RecordDbServerQuerySampleEvent.

client.port and network.peer.port remain 0 on MariaDB (all versions) and MySQL <8.0.22
because the only other source that exposes host:port is information_schema.PROCESSLIST,
which acquires a global mutex while iterating active threads — the same mutex held by
SHOW PROCESSLIST. This has negative performance consequences on busy systems, was
deprecated in MySQL 8.0, removed in MySQL 9.0, and was already removed from this
receiver in a prior change.

Assisted-by: Claude Sonnet 4.6
….1 fallback support

Remove the querySampleNoUserVars.tmpl template and associated code paths
that handled servers lacking performance_schema.user_variables_by_thread.
The minimum supported versions are now MySQL 5.7.x and MariaDB 10.5.x,
both of which have this table unconditionally.

Removes supportsUserVariablesByThread() predicate, the supportsUserVarsByThread
parameter from getQuerySamples, and the fallback template embed. Updates
README, tests, and integration test accordingly.

Assisted-by: Claude Sonnet 4.6
…om COMPATIBILITY.md

Remove the now-deleted predicate and querySampleNoUserVars.tmpl references.
Also correct supportsQuerySampleText minimum to 8.0.3 (was 8.0+).

Assisted-by: Claude Sonnet 4.6
…-version-support

- README.md: collapse redundant MariaDB minor-version rows to 10.5.x–10.11.x
  and 11.x (LTS: 11.4, 11.8)
- COMPATIBILITY.md: fill in Tested Platforms table with real live-test data
  (MySQL 8.4.7, 5.7.44, MariaDB 10.5.28, 11.8.2 — AWS RDS, 2026-04-21);
  add missing MariaDB 11.8 row; fix stale "all MariaDB 10.x" references
- chloggen: correct minimum versions to MySQL 5.7.3+ and MariaDB 10.5.2+;
  remove erroneous MySQL 5.6 entry

Assisted-by: Claude Sonnet 4.6
@cjksplunk cjksplunk requested a review from a team as a code owner April 21, 2026 13:23
@cjksplunk cjksplunk requested a review from mx-psi April 21, 2026 13:23
@cjksplunk cjksplunk marked this pull request as draft April 21, 2026 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DB Version and Maria incompatabilities

3 participants