Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
274b711
feat(rta): extend Real-Time Analytics to MySQL
theTibi Jun 16, 2026
0799543
feat(rta): full processlist raw data + hide-COMMIT toggle for MySQL
theTibi Jun 17, 2026
d52d9b1
fix(rta): per-type version gate, latency floor, collector tests + nits
theTibi Jun 17, 2026
93f4012
fix(rta): remove MySQL statement-latency collection floor
theTibi Jun 18, 2026
d3e3c47
fix(rta): preflight checks for MySQL collector with clear error status
theTibi Jun 19, 2026
bf0391f
fix(rta): address review — 3.9.0 gate, defensive support check, nits
theTibi Jun 19, 2026
c3e860b
feat(rta): inventory add/change API + CLI and API tests for MySQL RTA…
theTibi Jun 19, 2026
8b4f967
Merge branch 'main' into rta-mysql-test
theTibi Jun 19, 2026
54ad5d1
Merge remote-tracking branch 'origin/main' into rta-mysql-test
theTibi Jul 4, 2026
48e93be
Merge remote-tracking branch 'origin/main' into rta-mysql-test
theTibi Jul 31, 2026
8465a49
feat(rta): add database and user filters to the overview table
theTibi Jul 31, 2026
433dd46
fix(rta): address CodeRabbit review findings
theTibi Aug 1, 2026
6764b4e
Merge branch 'main' into rta-mysql-test
theTibi Aug 1, 2026
f281253
fix(rta): guard nil pmm-agent version; fix stale processlist doc
theTibi Aug 1, 2026
6e2d77a
feat(rta): pin Elapsed time to the right edge of the overview table
theTibi Aug 1, 2026
4a57085
chore(rta): align generated files with make gen output
theTibi Aug 2, 2026
1e03295
fix(rta): resolve golangci-lint findings in new RTA MySQL code
theTibi Aug 2, 2026
df2a3b3
feat(rta): implement comma-separated filter for Database and User col…
theTibi Aug 2, 2026
45a36bb
PMM-15283 Hide Database/User columns by default and compact Elapsed time
theTibi Aug 4, 2026
2b15eef
Merge branch 'main' into rta-mysql-test
theTibi Aug 4, 2026
d1740fc
PMM-15283 Keep millisecond precision in RTA Elapsed time
theTibi Aug 5, 2026
c7ca4cb
PMM-15283 Mark RTA technology, drop hand-pinning, scope Hide COMMIT
theTibi Aug 5, 2026
df1c662
Merge branch 'main' into rta-mysql-test
theTibi Aug 5, 2026
570998d
PMM-15283 Harden the MySQL RTA collector loop
theTibi Aug 6, 2026
84442d6
Merge branch 'main' into rta-mysql-test
theTibi Aug 6, 2026
bb3e1c7
Merge branch 'main' into rta-mysql-test
theTibi Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions admin/commands/inventory/add_agent_rta_mysql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (C) 2023 Percona LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package inventory

import (
"time"

"github.com/percona/pmm/admin/commands"
"github.com/percona/pmm/admin/pkg/flags"
"github.com/percona/pmm/api/inventory/v1/json/client"
agents "github.com/percona/pmm/api/inventory/v1/json/client/agents_service"
)

var addAgentRTAMySQLAgentResultT = commands.ParseTemplate(`
Real-Time Analytics MySQL agent added.
Agent ID : {{ .Agent.AgentID }}
PMM-Agent ID : {{ .Agent.PMMAgentID }}
Service ID : {{ .Agent.ServiceID }}
Username : {{ .Agent.Username }}
TLS enabled : {{ .Agent.TLS }}
Skip TLS verification : {{ .Agent.TLSSkipVerify }}

Disabled : {{ .Agent.Disabled }}
Custom labels : {{ formatCustomLabels .Agent.CustomLabels }}
Collect interval : {{ .Agent.RtaOptions.CollectInterval }}
Log level : {{ formatLogLevel .Agent.LogLevel }}
`)
Comment on lines +26 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether managed always returns rta_options, and how sibling RTA commands render it.
fd -t f -g '*rta*' admin/commands/inventory --exec rg -n -C3 'RtaOptions.CollectInterval'
rg -n -C6 'RTAOptions' managed/services/inventory/agents.go managed/services/converters.go

Repository: percona/pmm

Length of output: 7994


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- template definitions and handlers ---'
cat -n admin/commands/inventory/add_agent_rta_mysql.go | sed -n '1,115p'
cat -n admin/commands/inventory/change_agent_rta_mysql.go | sed -n '1,115p'
printf '%s\n' '--- generated response model ---'
rg -n -C5 'type RTAMySQLAgent|RtaOptions.*RTAOptions|type RTAOptions' api
printf '%s\n' '--- template behavior probe ---'
python3 - <<'PY'
from pathlib import Path
import re
for name in (
    "admin/commands/inventory/add_agent_rta_mysql.go",
    "admin/commands/inventory/change_agent_rta_mysql.go",
):
    text = Path(name).read_text()
    expr = re.search(r'\{\{ \.Agent\.RtaOptions\.CollectInterval \}\}', text)
    print(f"{name}: unguarded_expression={bool(expr)}")
print("Go text/template evaluates a nil intermediate pointer as an execution error; the template's remaining output is not a successful result.")
PY

Repository: percona/pmm

Length of output: 47124


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- RenderTemplate implementation and tests ---'
rg -n -C8 'func RenderTemplate|RenderTemplate\(' admin/commands
printf '%s\n' '--- response field declarations ---'
sed -n '7065,7090p' api/inventory/v1/json/client/agents_service/add_agent_responses.go
sed -n '7148,7170p' api/inventory/v1/json/client/agents_service/change_agent_responses.go

Repository: percona/pmm

Length of output: 50368


🏁 Script executed (no clone):

#!/bin/bash
set -e
if ! command -v go >/dev/null 2>&1; then
  echo "go unavailable"
  exit 0
fi
cat >/tmp/template_nil_probe.go <<'GO'
package main

import (
	"fmt"
	"os"
	"text/template"
)

type options struct {
	CollectInterval string
}
type agent struct {
	RtaOptions *options
}
type result struct {
	Agent *agent
}

func main() {
	t := template.Must(template.New("probe").Parse(
		"before\nCollect interval: {{ .Agent.RtaOptions.CollectInterval }}\nafter\n",
	))
	err := t.Execute(os.Stdout, result{Agent: &agent{}})
	fmt.Printf("\nexecute_error=%v\n", err)
}
GO
go run /tmp/template_nil_probe.go
rm -f /tmp/template_nil_probe.go

Length of output: 298


Guard RtaOptions before rendering CollectInterval. A missing RtaOptions causes commands.RenderTemplate to panic with a nil-pointer evaluation error. Apply the same conditional guard in both templates.

📍 Affects 2 files
  • admin/commands/inventory/add_agent_rta_mysql.go#L26-L39 (this comment)
  • admin/commands/inventory/change_agent_rta_mysql.go#L27-L39
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@admin/commands/inventory/add_agent_rta_mysql.go` around lines 26 - 39, The
addAgentRTAMySQLAgentResultT template in
admin/commands/inventory/add_agent_rta_mysql.go and the corresponding
change-agent template in admin/commands/inventory/change_agent_rta_mysql.go must
guard .Agent.RtaOptions before rendering CollectInterval. Apply the same
conditional template logic at both sites so missing RtaOptions renders safely
without a nil-pointer evaluation panic.


type addAgentRTAMySQLAgentResult struct {
Agent *agents.AddAgentOKBodyRtaMysqlAgent `json:"rta_mysql_agent"`
}

func (res *addAgentRTAMySQLAgentResult) Result() {}

func (res *addAgentRTAMySQLAgentResult) String() string {
return commands.RenderTemplate(addAgentRTAMySQLAgentResultT, res)
}

// AddAgentRTAMySQLAgentCommand is used by Kong for CLI flags and commands.
type AddAgentRTAMySQLAgentCommand struct {
PMMAgentID string `arg:"" help:"The pmm-agent identifier which runs this instance"`
ServiceID string `arg:"" help:"Service identifier"`
Username string `arg:"" optional:"" help:"MySQL username for getting queries data"`
Password string `help:"MySQL password for getting queries data"`
CustomLabels map[string]string `mapsep:"," help:"Custom user-assigned labels"`
SkipConnectionCheck bool `help:"Skip connection check"`
TLS bool `help:"Use TLS to connect to the database"`
TLSSkipVerify bool `help:"Skip TLS certificate verification"`
TLSCaFile string `help:"Path to certificate authority file"`
TLSCertFile string `help:"Path to client certificate file"`
TLSKeyFile string `help:"Path to client key file"`
CollectInterval *time.Duration `placeholder:"DURATION" help:"Query collect interval (default: server-defined 2s)"`

flags.LogLevelFatalFlags
}

// RunCmd executes the AddAgentRTAMySQLAgentCommand and returns the result.
func (cmd *AddAgentRTAMySQLAgentCommand) RunCmd() (commands.Result, error) {
customLabels := commands.ParseKeyValuePair(&cmd.CustomLabels)

tlsCa, err := commands.ReadFile(cmd.TLSCaFile)
if err != nil {
return nil, err
}

tlsCert, err := commands.ReadFile(cmd.TLSCertFile)
if err != nil {
return nil, err
}

tlsKey, err := commands.ReadFile(cmd.TLSKeyFile)
if err != nil {
return nil, err
}

params := &agents.AddAgentParams{
Body: agents.AddAgentBody{
RtaMysqlAgent: &agents.AddAgentParamsBodyRtaMysqlAgent{
PMMAgentID: cmd.PMMAgentID,
ServiceID: cmd.ServiceID,
Username: cmd.Username,
Password: cmd.Password,
CustomLabels: *customLabels,
SkipConnectionCheck: cmd.SkipConnectionCheck,
TLS: cmd.TLS,
TLSSkipVerify: cmd.TLSSkipVerify,
TLSCa: tlsCa,
TLSCert: tlsCert,
TLSKey: tlsKey,
LogLevel: cmd.LogLevel.EnumValue(),
},
},
Context: commands.Ctx,
}

if cmd.CollectInterval != nil {
params.Body.RtaMysqlAgent.RtaOptions = &agents.AddAgentParamsBodyRtaMysqlAgentRtaOptions{
CollectInterval: cmd.CollectInterval.String(),
}
}

resp, err := client.Default.AgentsService.AddAgent(params)
if err != nil {
return nil, err
}

return &addAgentRTAMySQLAgentResult{
Agent: resp.Payload.RtaMysqlAgent,
}, nil
}
221 changes: 221 additions & 0 deletions admin/commands/inventory/change_agent_rta_mysql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// Copyright (C) 2023 Percona LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package inventory

import (
"fmt"
"time"

"github.com/percona/pmm/admin/commands"
"github.com/percona/pmm/admin/pkg/flags"
"github.com/percona/pmm/api/inventory/v1/json/client"
agents "github.com/percona/pmm/api/inventory/v1/json/client/agents_service"
)

var changeAgentRTAMySQLAgentResultT = commands.ParseTemplate(`
Real-Time Analytics MySQL agent configuration updated.
Agent ID : {{ .Agent.AgentID }}
PMM-Agent ID : {{ .Agent.PMMAgentID }}
Service ID : {{ .Agent.ServiceID }}
Username : {{ .Agent.Username }}
TLS enabled : {{ .Agent.TLS }}
Skip TLS verification : {{ .Agent.TLSSkipVerify }}

Disabled : {{ .Agent.Disabled }}
Custom labels : {{ formatCustomLabels .Agent.CustomLabels }}
Collect interval : {{ .Agent.RtaOptions.CollectInterval }}
Log level : {{ formatLogLevel .Agent.LogLevel }}

{{- if .Changes}}
Configuration changes applied:
{{- range .Changes}}
- {{ . }}
{{- end}}
{{- end}}
`)

type changeAgentRTAMySQLAgentResult struct {
Agent *agents.ChangeAgentOKBodyRtaMysqlAgent `json:"rta_mysql_agent"`
Changes []string `json:"changes,omitempty"`
}

func (res *changeAgentRTAMySQLAgentResult) Result() {}

func (res *changeAgentRTAMySQLAgentResult) String() string {
return commands.RenderTemplate(changeAgentRTAMySQLAgentResultT, res)
}

// ChangeAgentRTAMySQLAgentCommand is used by Kong for CLI flags and commands.
type ChangeAgentRTAMySQLAgentCommand struct {
// Embedded flags
flags.LogLevelFatalChangeFlags

AgentID string `arg:"" help:"Real-Time Analytics MySQL Agent ID"`

// NOTE: Only provided flags will be changed, others will remain unchanged

// Basic options
Enable *bool `help:"Enable or disable the agent"`
Username *string `help:"MySQL username for getting queries data"`
Password *string `help:"MySQL password for getting queries data"`

// TLS options
TLS *bool `help:"Use TLS to connect to the database"`
TLSSkipVerify *bool `help:"Skip TLS certificate verification"`
TLSCaFile *string `help:"Path to certificate authority file"`
TLSCertFile *string `help:"Path to client certificate file"`
TLSKeyFile *string `help:"Path to client key file"`

// RTA specific options
CollectInterval *time.Duration `placeholder:"DURATION" help:"Query collect interval (default: server-defined 2s)"`

// Custom labels
CustomLabels *map[string]string `mapsep:"," help:"Custom user-assigned labels"`

SkipConnectionCheck *bool `help:"Skip connection check"`
}

// RunCmd executes the ChangeAgentRTAMySQLAgentCommand and returns the result.
func (cmd *ChangeAgentRTAMySQLAgentCommand) RunCmd() (commands.Result, error) {
var changes []string

// Parse custom labels if provided
customLabels := commands.ParseKeyValuePair(cmd.CustomLabels)

// Read TLS files if provided
var tlsCa, tlsCert, tlsKey *string

if cmd.TLSCaFile != nil {
content, err := commands.ReadFile(*cmd.TLSCaFile)
if err != nil {
return nil, fmt.Errorf("failed to read TLS CA file: %w", err)
}

tlsCa = &content
}

if cmd.TLSCertFile != nil {
content, err := commands.ReadFile(*cmd.TLSCertFile)
if err != nil {
return nil, fmt.Errorf("failed to read TLS certificate file: %w", err)
}

tlsCert = &content
}

if cmd.TLSKeyFile != nil {
content, err := commands.ReadFile(*cmd.TLSKeyFile)
if err != nil {
return nil, fmt.Errorf("failed to read TLS key file: %w", err)
}

tlsKey = &content
}

body := &agents.ChangeAgentParamsBodyRtaMysqlAgent{
Enable: cmd.Enable,
Username: cmd.Username,
Password: cmd.Password,
TLS: cmd.TLS,
TLSSkipVerify: cmd.TLSSkipVerify,
TLSCa: tlsCa,
TLSCert: tlsCert,
TLSKey: tlsKey,
LogLevel: convertLogLevelPtr(cmd.LogLevel),
SkipConnectionCheck: cmd.SkipConnectionCheck,
}

if customLabels != nil {
body.CustomLabels = &agents.ChangeAgentParamsBodyRtaMysqlAgentCustomLabels{
Values: *customLabels,
}
}

if cmd.CollectInterval != nil {
body.RtaOptions = &agents.ChangeAgentParamsBodyRtaMysqlAgentRtaOptions{
CollectInterval: cmd.CollectInterval.String(),
}
}

params := &agents.ChangeAgentParams{
AgentID: cmd.AgentID,
Body: agents.ChangeAgentBody{
RtaMysqlAgent: body,
},
Context: commands.Ctx,
}

resp, err := client.Default.AgentsService.ChangeAgent(params)
if err != nil {
return nil, err
}

// Track changes
if cmd.Enable != nil {
if *cmd.Enable {
changes = append(changes, "enabled agent")
} else {
changes = append(changes, "disabled agent")
}
}
if cmd.Username != nil {
changes = append(changes, "updated username")
}
if cmd.Password != nil {
changes = append(changes, "updated password")
}
if cmd.TLS != nil {
if *cmd.TLS {
changes = append(changes, "enabled TLS")
} else {
changes = append(changes, "disabled TLS")
}
}
if cmd.TLSSkipVerify != nil {
if *cmd.TLSSkipVerify {
changes = append(changes, "enabled TLS skip verification")
} else {
changes = append(changes, "disabled TLS skip verification")
}
}
if cmd.TLSCaFile != nil {
changes = append(changes, "updated TLS CA certificate")
}
if cmd.TLSCertFile != nil {
changes = append(changes, "updated TLS certificate")
}
if cmd.TLSKeyFile != nil {
changes = append(changes, "updated TLS key")
}
if cmd.LogLevel != nil {
changes = append(changes, fmt.Sprintf("changed log level to %s", *cmd.LogLevel))
}
if customLabels != nil {
if len(*customLabels) != 0 {
changes = append(changes, "updated custom labels")
} else {
changes = append(changes, "custom labels are removed")
}
}

if cmd.CollectInterval != nil {
changes = append(changes, fmt.Sprintf("changed collect interval to %s", *cmd.CollectInterval))
}

return &changeAgentRTAMySQLAgentResult{
Agent: resp.Payload.RtaMysqlAgent,
Changes: changes,
}, nil
}
2 changes: 2 additions & 0 deletions admin/commands/inventory/inventory.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type AddAgentCommand struct {

RDSExporter AddAgentRDSExporterCommand `cmd:"" help:"Add rds_exporter to inventory"`
RTAMongoDBAgent AddAgentRTAMongoDBAgentCommand `cmd:"" name:"rta-mongodb-agent" help:"Add Real-Time Analytics MongoDB agent to inventory"`
RTAMySQLAgent AddAgentRTAMySQLAgentCommand `cmd:"" name:"rta-mysql-agent" help:"Add Real-Time Analytics MySQL agent to inventory"`
}

// AddNodeCommand is used by Kong for CLI flags and commands.
Expand Down Expand Up @@ -119,6 +120,7 @@ type ChangeAgentCommand struct {
QANPostgreSQLPgStatementsAgent ChangeAgentQANPostgreSQLPgStatementsAgentCommand `cmd:"" name:"qan-postgresql-pgstatements-agent" help:"Change QAN PostgreSQL pgstatements agent configuration (only passed flags will be changed)"`
QANPostgreSQLPgStatMonitorAgent ChangeAgentQANPostgreSQLPgStatMonitorAgentCommand `cmd:"" name:"qan-postgresql-pgstatmonitor-agent" help:"Change QAN PostgreSQL pgstatmonitor agent configuration (only passed flags will be changed)"`
RTAMongoDBAgent ChangeAgentRTAMongoDBAgentCommand `cmd:"" name:"rta-mongodb-agent" help:"Change Real-Time Analytics MongoDB agent configuration (only passed flags will be changed)"`
RTAMySQLAgent ChangeAgentRTAMySQLAgentCommand `cmd:"" name:"rta-mysql-agent" help:"Change Real-Time Analytics MySQL agent configuration (only passed flags will be changed)"`
}

// formatTypeValue checks acceptable type value and variations contains input and returns type value.
Expand Down
14 changes: 13 additions & 1 deletion admin/commands/inventory/list_agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ var acceptableAgentTypes = map[string][]string{
types.AgentTypeQANPostgreSQLPgStatMonitorAgent: {types.AgentTypeName(types.AgentTypeQANPostgreSQLPgStatMonitorAgent), "qan-postgresql-pgstatmonitor-agent"},
types.AgentTypeRDSExporter: {types.AgentTypeName(types.AgentTypeRDSExporter), "rds-exporter"},
types.AgentTypeRTAMongoDBAgent: {types.AgentTypeName(types.AgentTypeRTAMongoDBAgent), "rta-mongodb-agent"},
types.AgentTypeRTAMySQLAgent: {types.AgentTypeName(types.AgentTypeRTAMySQLAgent), "rta-mysql-agent"},
}

type listResultAgent struct {
Expand Down Expand Up @@ -141,7 +142,8 @@ func (cmd *ListAgentsCommand) RunCmd() (commands.Result, error) {
len(agentsRes.Payload.QANPostgresqlPgstatementsAgent)+
len(agentsRes.Payload.QANPostgresqlPgstatmonitorAgent)+
len(agentsRes.Payload.ExternalExporter)+
len(agentsRes.Payload.RtaMongodbAgent),
len(agentsRes.Payload.RtaMongodbAgent)+
len(agentsRes.Payload.RtaMysqlAgent),
)
for _, a := range agentsRes.Payload.PMMAgent {
status := "disconnected"
Expand Down Expand Up @@ -309,6 +311,16 @@ func (cmd *ListAgentsCommand) RunCmd() (commands.Result, error) {
Disabled: a.Disabled,
})
}
for _, a := range agentsRes.Payload.RtaMysqlAgent {
agentsList = append(agentsList, listResultAgent{
AgentType: types.AgentTypeRTAMySQLAgent,
AgentID: a.AgentID,
PMMAgentID: a.PMMAgentID,
ServiceID: a.ServiceID,
Status: getAgentStatus(a.Status),
Disabled: a.Disabled,
})
}

return &listAgentsResult{
Agents: agentsList,
Expand Down
Loading
Loading