Observability

Monitoring

Bloodraven exposes Prometheus metrics, a REST status API, and a WebSocket endpoint for real-time status streaming. For the structured-log field set and the stable msg vocabulary that downstream log pipelines key off of, see the Log schema contract.

Setup guidesUse Prometheus Setup and Grafana Dashboards for installation. This page is the complete observability reference.

Minimum alert set

SignalWhy it mattersRunbook
Operator downReconciliation and DNS updates stopOperator unavailable
No writable siteApplication writes are unavailableEmergency manual promotion
Split-brain detectedMore than one site may accept writesSplit-brain recovery
Replication lag highRPO risk is increasingReplication lag high
Divergent transactionsA site holds transactions the primary lacks: an old primary cannot safely rejoin, or a replica (often replicating normally) is excluded from ordered updates and planned failoversDivergent recovery, Divergent replicas
Backup staleRecovery point is agingFailed backup
Verification staleBackups are not proven restorableBackup Verification
PITR archive laggingPITR RPO target may be missedBackup And Restore
Dragonfly degradedCache/session continuity may be unavailable during planned failoverPlayground Dragonfly co-management

Operator health

Check these before debugging individual failover groups:

kubectl rollout status deployment/bloodraven -n bloodraven
kubectl logs -n bloodraven deploy/bloodraven
kubectl port-forward -n bloodraven deploy/bloodraven 8080:8080
curl http://localhost:8080/metrics | grep '^bloodraven_'

Prometheus metrics

Metrics are served on :8080/metrics in standard Prometheus exposition format.

Available metrics

Deployment lease metrics are listed in the Deployment API metric reference. Active lease counts are dimensionless; age is seconds; grants, expirations, revocations, and deferrals are event counts. The bounded labels are kind (migration, failover-hold), grant result (four values), and revocation reason (TopologyChanged, OperatorRevoked). group scales with managed group names; operationId, instance, and token material are never metric labels. Per group, active/age/expiry families have at most two kind series, grants have at most eight kind/result series, and revocations have at most four kind/reason series. The deferral counter uses a bounded controller reason such as DeploymentHold. These families have no namespace label; same-named groups in different namespaces cannot be distinguished by them.

MetricTypeLabelsDescription
bloodraven_poll_latency_secondsHistogramsiteDuration of each MySQL probe. A site in probe backoff records nothing on the polls that skip it.
bloodraven_state_transitions_totalCountersite, from, toCount of state transitions per site. from and to are kebab-case site states, as on bloodraven_site_state.
bloodraven_taint_operations_totalCountersite, actionCount of node taint/untaint operations
bloodraven_dns_flips_totalCountersiteCount of DNSEndpoint updates (DNS record flips)
bloodraven_failovers_totalCountertarget_siteTotal number of failovers executed. Incremented after successful MySQL promotion.
bloodraven_keyring_promotions_blocked_totalCounternamespace, group, site, outcomePromotion decisions that skipped or refused a site because it is mid-keyring-rotation. outcome is skipped or refused. Incremented once per decision, not per Event reminder.
bloodraven_primary_reassert_totalCountersiteTimes the operator restored writability on the promoted primary after finding it fenced with no writable site remaining (typically the target's own sidecar re-fenced it with a stale lease right after a promotion, or the primary's MySQL restarted and came back with the read_only=ON every mysqld boots with). A steadily increasing counter means something keeps fencing the primary โ€” check the sidecars' connectivity to the operator's auxiliary Service.
bloodraven_websocket_connected_clientsGauge--Number of currently connected WebSocket clients
bloodraven_replication_lag_secondsGaugenamespace, group, site, roleReplication lag in seconds on a follower site. -1 if lag is NULL (not replicating). Only present for replica sites. role is the kebab-case of spec.sites[].role: primary-candidate, disaster-recovery, or read-only.
bloodraven_replication_runningGaugenamespace, group, site, role, threadWhether a replication thread is running (1=yes, 0=no). Thread is io or sql. Only present for replica sites.
bloodraven_restore_reclone_pendingGaugenamespace, group, site, phase1 for each site a full-instance in-place restore has not recloned yet (status.restoreReclones.pending); phase is the kebab-case of the queue phase (armed, draining, held). Such a site's replication receiver is stopped by design until its reclone, so exclude it from replication alerts (see BloodravenReplicationDown). The series is removed when the site leaves the queue.
bloodraven_replication_source_stateGaugenamespace, group, site, stateDirect-source convergence state set for each follower, scoped by failover group. state is one of converged, pending, or blocked; one series is 1 and the others are 0. Cleared for the active primary.
bloodraven_site_stateGaugenamespace, group, site, role, stateCurrent site state as a state-set: 1 for the current state, 0 for others. state is the kebab-case of status.sites[].state: writable, read-only, unreachable, or unknown. role is the kebab-case of spec.sites[].role.
bloodraven_divergent_transactionsGaugesiteNumber of transactions on a site that the primary lacks: a returning old primary after emergency failover, or a replica found by the replica GTID audit (which runs whatever the replication threads report). 0 when healthy. Non-zero means the site is RecoveryBlocked: it is not promoted by an ordered update or planned failover, and Degraded is True with reason DivergentTransactions.
bloodraven_archiver_upload_failuresGaugenamespace, group, siteCumulative PITR archiver upload failures reported by the per-site sidecar, including one per failed archive store construction attempt. Monotonic except across sidecar restarts โ€” use increase() / rate() in dashboards.
bloodraven_archiver_last_upload_timestamp_secondsGaugenamespace, group, siteUnix timestamp of the last successful PITR binlog archive per site. 0 if nothing archived yet.
bloodraven_archiver_backlog_filesGaugenamespace, group, siteSealed binlogs present in the MySQL index but missing from the archiver manifest at the end of the last scan. >0 means archival is falling behind.
bloodraven_backup_verified_timestamp_secondsGaugegroup, profileUnix timestamp of the last Succeeded MysqlBackupVerification per profile. Anchor staleness alerts on this gauge โ€” a fresh bloodraven_backup_last_success_timestamp_seconds without a fresh verification means nobody has proven the backup can be restored.
bloodraven_backup_verification_last_attempt_timestamp_secondsGaugegroup, profileUnix timestamp of the last terminal verification attempt, regardless of result. Distinguishes "verification never ran" from "verification ran but failed".
bloodraven_backup_verification_runs_totalCountergroup, profile, resultTerminal verification attempts labelled success or failure.
bloodraven_backup_verification_duration_secondsHistogramgroup, profileWall-clock duration of a verification run.
bloodraven_backup_verification_replay_lag_secondsGaugegroup, profileFor successful mode: Latest PITR verifications only: completionTime โˆ’ pointInTime.newestReplayedCommitTime. Timestamp drills never set it, because their newest replayed event is a historical target. A rising value means archived binlogs trail the live primary โ€” alert below your RPO target.
bloodraven_restore_duration_secondsHistogramnamespace, group, restore_kind, target_siteData-plane duration of successful restore Jobs. restore_kind is init_from_backup or in_place; duration starts at Job status.startTime when available and ends when the operator observes terminal success.
bloodraven_restore_last_success_timestamp_secondsGaugenamespace, group, restore_kind, target_siteUnix timestamp of the last successful restore Job observation.
bloodraven_restore_last_source_size_bytesGaugenamespace, group, restore_kind, target_siteSource backup artifact size in bytes for the most recent successful restore when known from MysqlBackup.status.sizeBytes. Direct S3/PVC restores and unknown sizes clear/omit this series so an older known size is not reported as current.
bloodraven_dragonfly_site_upGaugegroup, siteDragonfly site reachability from the operator's latest INFO replication poll (1 reachable, 0 unreachable).
bloodraven_dragonfly_promotions_totalCountergroup, target_site, resultDragonfly promotion attempts labelled success, failed, skipped, or sessions_lost. success is a REPLTAKEOVER that preserved continuity. sessions_lost is a successful REPLICAOF NO ONE fallback โ€” the target is writable but session continuity is not guaranteed. failed means the target was not promoted.
bloodraven_dragonfly_manager_panics_totalCounternamespace, namePanics recovered inside the Dragonfly manager polling loop. Any increase should be investigated.
bloodraven_license_infoGaugenamespace, group, organization, edition, validConstant-1 info series for the license observed on a failover group. edition is community, production, or organization. valid is the string true or false. No license is edition="community", valid="true". An invalid token is edition="community", valid="false". An ended update period stays valid="true". One series per group.
bloodraven_license_updates_expiry_timestamp_secondsGaugenamespace, group, organization, editionUnix timestamp when the paid update period ends. Absent for community and invalid licenses. A past value is the supported perpetual state. Alert with (bloodraven_license_updates_expiry_timestamp_seconds - time()) / 86400 < 30.

Dragonfly replica full-resync state is exposed on the CR as status.dragonfly.sites[].syncInProgress, with companion fields linkStatus, lastIOSecondsAgo, and ready. syncInProgress and lastIOSecondsAgo are absent when Dragonfly's INFO replication did not report them. If you export custom-resource status with kube-state-metrics or another CRD status adapter, alert when syncInProgress remains true for longer than the expected warm-up window or when it toggles repeatedly outside planned failovers and Dragonfly image rollouts. Frequent full resyncs can spike master latency and reduce planned failover session-preservation confidence.

Scrape configuration

Add a scrape job for the operator:

# prometheus.yml
scrape_configs:
  - job_name: bloodraven
    kubernetes_sd_configs:
      - role: pod
        namespaces:
          names: [bloodraven]
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
        regex: bloodraven
        action: keep
      - source_labels: [__meta_kubernetes_pod_container_port_number]
        regex: "8080"
        action: keep

Or if using the Prometheus Operator, create a PodMonitor:

apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: bloodraven
  namespace: bloodraven
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: bloodraven
  podMetricsEndpoints:
    - port: metrics
      interval: 15s

Alerting rules

Recommended alerts:

groups:
  - name: bloodraven
    rules:
      # Site has no healthy primary
      - alert: BloodravenNoPrimary
        expr: |
          count by (site) (
            bloodraven_state_transitions_total{to="writable"}
          ) == 0
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Site {{ $labels.site }} has no writable primary"

      # High poll latency may indicate network issues
      - alert: BloodravenHighPollLatency
        expr: |
          histogram_quantile(0.99, rate(bloodraven_poll_latency_seconds_bucket[5m])) > 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Poll latency to {{ $labels.site }} exceeds 1 second (p99)"

      # Failover occurred
      - alert: BloodravenFailoverOccurred
        expr: increase(bloodraven_failovers_total[5m]) > 0
        labels:
          severity: warning
        annotations:
          summary: "Failover occurred: {{ $labels.target_site }} promoted as new primary"

      # Replication lag exceeds threshold on a data-bearing replica.
      # role=~"primary-candidate|disaster-recovery" pages RPO risk and stays
      # silent on role="read-only" readers, whose lag is designed (they are shed
      # from mysql-<group>-replicas). For promotable replicas only, use
      # role="primary-candidate".
      - alert: BloodravenReplicationLagging
        expr: bloodraven_replication_lag_seconds{role=~"primary-candidate|disaster-recovery"} > 300
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "Replication lag on {{ $labels.group }}/{{ $labels.site }} ({{ $labels.role }}) is {{ $value }}s"

      # A site holds transactions the primary lacks (old primary after an
      # emergency failover, or a replica found by the GTID audit)
      - alert: BloodravenDivergentTransactions
        expr: bloodraven_divergent_transactions > 0
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.site }} has {{ $value }} transactions the primary lacks โ€” reclone it or make the primary contain them; ordered updates and planned failovers will not promote it"

      # Replication thread down on a data-bearing replica. Sites awaiting
      # their post-restore reclone have their receiver stopped by design.
      - alert: BloodravenReplicationDown
        expr: |
          bloodraven_replication_running{role=~"primary-candidate|disaster-recovery"} == 0
          unless on(namespace, group, site) bloodraven_restore_reclone_pending == 1
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Replication {{ $labels.thread }} thread stopped on {{ $labels.group }}/{{ $labels.site }} ({{ $labels.role }})"

      # A failed full-instance restore left peers held with their
      # replication stopped; an administrator must decide what to keep.
      - alert: BloodravenRestoreReclonesHeld
        expr: bloodraven_restore_reclone_pending{phase="held"} == 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.group }}/{{ $labels.site }} is held after a failed in-place restore: no replication, not promotable"

      # No writable site in a failover group
      - alert: BloodravenNoWritableSite
        expr: |
          max by (namespace, group) (bloodraven_site_state{state="writable"}) == 0
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: "No site is currently writable in {{ $labels.namespace }}/{{ $labels.group }}"

      # Dragonfly site unavailable; MySQL remains authoritative, but planned
      # session/cache continuity is at risk.
      - alert: BloodravenDragonflySiteDown
        expr: bloodraven_dragonfly_site_up == 0
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "Dragonfly site {{ $labels.site }} is unreachable for {{ $labels.group }}"

      # Failed Dragonfly promotion means the MySQL failover may have succeeded
      # with cache unavailable.
      - alert: BloodravenDragonflyPromotionFailed
        expr: increase(bloodraven_dragonfly_promotions_total{result="failed"}[5m]) > 0
        labels:
          severity: warning
        annotations:
          summary: "Dragonfly promotion failed for {{ $labels.group }} target {{ $labels.target_site }}"

      # Empty-master fallback: target is writable but REPLTAKEOVER did not run.
      - alert: BloodravenDragonflySessionsLost
        expr: increase(bloodraven_dragonfly_promotions_total{result="sessions_lost"}[5m]) > 0
        labels:
          severity: warning
        annotations:
          summary: "Dragonfly promoted {{ $labels.target_site }} via REPLICAOF NO ONE for {{ $labels.group }}; session continuity is not guaranteed"

      # A recovered panic keeps the manager alive, but it is still a bug signal.
      - alert: BloodravenDragonflyManagerPanic
        expr: increase(bloodraven_dragonfly_manager_panics_total[5m]) > 0
        labels:
          severity: critical
        annotations:
          summary: "Dragonfly manager recovered a panic for {{ $labels.namespace }}/{{ $labels.name }}"

Grafana dashboards

Bloodraven ships five ready-to-use Grafana dashboards covering every metric the operator publishes. They live in the chart at charts/bloodraven/dashboards/ and are also installable as ConfigMaps via the Helm chart.

DashboardUIDWhat it's for
Overviewbloodraven-overviewHealth at a glance: writable sites, state timeline, failover activity, replication lag, backup/archiver freshness. Start here.
Failover & Topologybloodraven-failoverAuto and planned failovers, durations, lag-wait histograms, state transitions, DNS flips, node taints, split-brain resolves.
Replication & Recoverybloodraven-replicationReplication lag per site, IO/SQL thread up/down, divergent transactions, reclone ops, poll latency.
Backups & Verificationbloodraven-backupsBackup run/failure counts, duration, size, last-success age, verification runs, PITR replay lag.
PITR Archiverbloodraven-archiverPer-site archiver upload age, backlog files, upload failures.

All five dashboards share a datasource variable and cross-link in the top-left corner, so you can roam between them without losing the time range.

Setup โ€” three paths

Pick whichever matches your Grafana install. All three use the same JSON files.

1. Helm chart + kube-prometheus-stack (zero-config)

If your Grafana is deployed by kube-prometheus-stack or the upstream grafana/grafana chart, its dashboard-sidecar watches for ConfigMaps labelled grafana_dashboard: "1" by default. Just enable the flag:

helm upgrade --install bloodraven bloodraven/bloodraven \
  --namespace bloodraven --create-namespace \
  --set grafanaDashboards.enabled=true

The chart renders one ConfigMap per dashboard, the sidecar picks them up within ~30s, and the dashboards appear in a "Bloodraven" folder in Grafana. No restart, no re-login.

Common tweaks in values.yaml:

grafanaDashboards:
  enabled: true
  # Sidecar is often scoped to the monitoring namespace
  namespace: monitoring
  # Change folder name
  folder: MySQL / Bloodraven
  # If your sidecar watches a non-default label
  label: grafana_dashboard
  labelValue: "1"

2. Grafana file-based provisioning

If you provision Grafana from disk, copy the JSON files into your provisioning directory and point a provider at them:

# One-time copy (re-run on upgrade to get dashboard updates)
kubectl -n monitoring cp -c grafana \
  bloodraven/charts/bloodraven/dashboards \
  grafana-pod:/var/lib/grafana/dashboards/bloodraven
# /etc/grafana/provisioning/dashboards/bloodraven.yaml
apiVersion: 1
providers:
  - name: bloodraven
    folder: Bloodraven
    type: file
    disableDeletion: false
    updateIntervalSeconds: 30
    allowUiUpdates: true
    options:
      path: /var/lib/grafana/dashboards/bloodraven

3. Manual UI import

For one-off installs:

  1. In Grafana, click Dashboards โ†’ New โ†’ Import.
  2. Paste the contents of any file in charts/bloodraven/dashboards/.
  3. Pick your Prometheus datasource when prompted.
  4. Repeat for each dashboard you want.

Cross-dashboard links use the dashboard UID (bloodraven-overview, etc.) so they keep working as long as you don't edit the UID on import.

Prerequisites

  • Your Prometheus must be scraping the operator โ€” either the ServiceMonitor the chart ships (--set metrics.serviceMonitor.enabled=true) or an equivalent scrape job.
  • The dashboards use standard Prometheus histograms/gauges, so no recording rules are required. Alerts in the next section can sit alongside them.

Kubernetes Events

Bloodraven records standard Kubernetes Events on all five of its kinds: MysqlFailoverGroup, MysqlBackup, MysqlBackupVerification, MysqlDatabase and MysqlStandbyCluster. These events can be forwarded to Slack, PagerDuty, or any webhook endpoint using tools like Kubewatch, Argo Events, or Event Router.

The tables below list every Event reason the operator records, with the one type (Normal or Warning) it is always recorded with. They are generated from the reason registry in api/v1alpha1/events.go. Reasons are named <Subject><PastTenseVerb>, optionally followed by a qualifier that names the cause: Succeeded and Failed for an operation, Rejected for a request that is not admitted, Refused for an automatic action blocked for safety. An Event that accompanies a status condition and repeats that condition's reason (a condition mirror) keeps the condition's reason instead.

Topology and failover

Recorded on MysqlFailoverGroup.

ReasonTypeDescription
FailoverExecutedNormalA failover completed and a new primary was promoted.
DataLossDetectedWarningDivergent transactions found on old primary after emergency failover.
DivergentReplicaDetectedWarningThe replica GTID audit found a replica holding transactions the primary lacks; emitted on first detection and when the count changes.
DivergenceResolvedNormalA replica's divergence report was cleared by a fresh containment proof (after a reclone, or after the primary was made to contain the set).
DivergentSitePromotedWarningA site carrying a divergence report became the primary (an emergency failover with no clean candidate); its extra transactions are now authoritative. Also recorded as the DivergentPromotion condition. Repeats the condition reason.
OldPrimaryRecoveredNormalThe old primary recovered after a failover and is now replicating from the new primary.
SplitBrainDetectedWarningBoth sites are writable (split brain). Also emitted when a second site answers writable before the debounce confirms it; the message names the unconfirmed sites, and fencing waits for the confirmation.
NoPrimaryDetectedWarningBoth sites are read-only (no primary). Not emitted when the cause is a mid-rotation or pending-reclone promotion refusal (that is KeyringPromotionRefused or PromotionRefusedReclonePending), or while the group is Initializing after an operator start.
TotalLossDetectedWarningBoth sites are unreachable.
SiteRecoveredNormalDegraded condition cleared, topology is healthy. Not emitted when the group leaves Initializing, which is not a degraded state.
PromotionRefusedReclonePendingWarningAutomatic promotion refused: the remaining candidates have not been recloned since a full-instance in-place restore and still hold pre-restore data (status.restoreReclones.pending). The group stays without a writable primary until the old primary returns, or until status.restoreReclones is removed to accept the stale copy (see Peer reclones).
TopologyManagerStartFailedWarningThe group has no running topology manager (no polling, status updates or failover) because starting one keeps failing, usually over an operator or TLS Secret that is missing, an operator Secret without a username or password key, or TLS material that will not load. The message carries the error; the runner retries every 30s. Emitted on the first failure after a running manager was torn down, otherwise once the failure outlives one retry; then at most once per 5 minutes (a changed error) or every 10 minutes (unchanged). The failed to start topology manager log line still fires on every attempt.
TopologyManagerStartedNormalA topology manager started after a TopologyManagerStartFailed Warning; the message gives the number of failed attempts. The operator keeps this history in memory only: after an operator restart a recovery is not announced, so neither Event proves the current state โ€” the failed to start topology manager log line, repeated every 30s while the group has no manager, does.
OrderedUpdateStalledWarningAn ordered update stopped on a site whose pod is not Ready: one it just rolled, or one already on the new pod template. Nothing else is rolled or handed off until that pod has been Ready continuously for 30s. The message (also the Updating condition's) names the site, pod and cause. Emitted once per stall, not on every retry; a crash loop reads the same (crash looping, last exit code โ€ฆ) in every phase of the loop. See Ordered updates. Repeats the condition reason.
RecloneRequestedNormalA valid bloodraven.shipstream.io/reclone-site annotation was accepted (an administrator's, or the in-place restore's reclone queue); CLONE INSTANCE will start on the next poll, or once a planned failover, ordered update, promotion or in-place restore holding the sites has finished.
RecloneRejectedWarningThe reclone annotation failed the safety interlock, or named a site whose post-restore reclone queue is Armed or Held. The interlock refuses a value that does not parse as <site>:confirm=<group>:uid=<group UID>[:gtidPrefix=<prefix>] (no site, an unknown key, a bare site name), a confirm= or uid= that does not name this group, an unknown site, and a divergent-GTID prefix that is missing, too short or mismatched, or given for a site with no recorded divergence. The message names the full value to set. The annotation is cleared so the administrator can retry.
BootstrapRefusedRestoreHoldWarningA clone into a site was refused because an in-place restore holds it (its reclone queue is Armed or Held, or the topology is frozen). The site keeps its data and its stopped replication.
SidecarVersionSkewWarningThe group's sidecar image (a spec.sidecar.image pin, or the operator's default BLOODRAVEN_SIDECAR_IMAGE when it pins none) is tagged with a different release than the running operator. The two share a rendering contract and are supported only at matching releases; on a group with spec.tls, an older sidecar cannot reach MySQL at all. Advisory only: the operator never blocks on it, and it is not emitted for digest-pinned or untagged images.
MysqlConfIgnoredWarningEntries of the group or site spec.mysqlConf were left out of the rendered my.cnf: a key outside [A-Za-z0-9_.-]+ or a value with a line break, or a key that would override an operator-owned setting, including through a loose-, skip-, disable-, enable- or maximum- spelling. The message names the site and the keys; remove them from spec.mysqlConf. Admission refuses the malformed keys and values, so only an object stored before that rule reaches the first case. Also emitted, without a site, when spec.backup.pitr.maxBinlogSize is not a byte count with an optional k, m or g suffix (likewise refused at admission): my.cnf gets the 100M default instead.

Refused workloads and metadata

Recorded on MysqlFailoverGroup and MysqlBackupVerification.

The operator refuses to act on something until a human fixes it, and does not retry in a loop. Each Event repeats the condition reason it sets and is recorded once per finding.

ReasonTypeDescription
WorkloadSelectorMismatchWarningA site's Deployment or Dragonfly StatefulSet has a selector other than the one the operator renders (for example an object left from an older release with other label keys). The selector is immutable, so the operator does not update the object and reports Degraded=True/WorkloadSelectorMismatch; the message names the object. Delete it so the operator re-creates it (a MySQL site keeps its PVC). Repeats the condition reason.
MetadataVersionUnsupportedWarningAn internal annotation payload carries a schemaVersion this operator does not know (a newer operator wrote it, or it was edited). It proves nothing: on a MysqlFailoverGroup (the planned-failover terminal marker, a deployment lease record) it is reported as Degraded=True/MetadataVersionUnsupported and the payload is refused, except that a terminal marker naming a run with outcome ManualRecoveryRequired still holds automatic failover. An unreadable lease record refuses lease requests (the deployment API answers 423 unstable with this reason), the admission of planned operations, and the reopening of admission after a topology change, until the Lease is deleted or an operator that reads it runs; on a MysqlBackupVerification (the verification Job's source record) the run fails with Verified=False/MetadataVersionUnsupported. Repeats the condition reason.

Planned failover

Recorded on MysqlFailoverGroup.

See Planned failover for the phases, outcomes and the automation hold.

ReasonTypeDescription
PlannedFailoverStartedNormalA planned-failover request was accepted, or a deferred one resumed; the message gives the source, the target, maxLagWait and drainTimeout. The run enters Admitting.
PlannedFailoverSkippedNormalThe planned-failover target is already the active primary, so nothing is done and the request is removed. A deferred run that resolves this way then ends NotStarted with AlreadyActive, which also emits PlannedFailoverRejected.
PlannedFailoverRejectedWarningA planned-failover request was refused and the annotation removed: its value is invalid, the run ended NotStarted (the message gives the reason, for example TargetUnhealthy, CooldownActive or AdmissionTimeout), another run is still running, an automation hold stands, or an earlier run's fenced source has not healed. Nothing was fenced; fix the cause and re-annotate. See Planned failover outcomes.
PlannedFailoverDeferredNormalPlanned switchover waits for cooldown or a deployment hold. Deferred/DeploymentHold status names the blocking operation and instance and gives an expiry-based retry time. No action is required while an expected deployment is progressing; investigate an unexpectedly long hold.
PlannedFailoverAdmissionDelayedNormalA planned failover is waiting to be admitted; the message gives the admission.waitReason (for example PeerUnreadable, TargetNotReady, UpdateInFlight or AuthorityUnsettled) and its detail. Emitted when the wait reason or its detail changes. Nothing is fenced while it waits, and the run ends NotStarted with AdmissionTimeout if it is not admitted within 2 minutes.
PlannedFailoverClaimGrantedNormalThe planned failover was granted the claim; the message names the run, source and target. The topology manager defers emergency failover, ordered updates and other topology changes while the run holds it, and fencing the source starts next.
PlannedFailoverDrainStartedNormalThe source is fenced (super_read_only=ON) and the planned failover is closing application connections on it, for at most drainTimeout. Writes are paused from here until the target is promoted or the run rolls back.
PlannedFailoverTargetCaughtUpNormalThe target applied every transaction in the fenced set; the message gives how long that took. The run promotes the target next, after syncing Dragonfly when it is enabled.
PlannedFailoverDNSUpdateFailedWarningA planned failover promoted its target but the DNS update failed; the message carries the error. The topology manager retries the update, and the run records the DNSUpdateFailed warning on its result. Check the DNS provider if clients still resolve the old primary.
PlannedFailoverRollbackStartedWarningA planned failover failed before anything was written to its target and entered RollingBack; the message gives the reason (for example LagTimeout, FenceFailed, SourceLost, DivergentTarget, DragonflySyncTimeout or DragonflyPromotionFailed). The operator unfences the source unless another site is writable; the run then ends Failed, normally RolledBack or SourceLeftFenced, with PlannedFailoverFailed.
PlannedFailoverSucceededNormalThe planned failover completed: the target is the primary and status.lastFailover records it. The message lists any warnings the run recorded, such as DNSUpdateFailed, or none.
PlannedFailoverFailedWarningA planned failover ended Failed with outcome RolledBack, SourceLeftFenced or PromotedUnconfirmed; the message gives the outcome, reason and status message. NotStarted runs emit PlannedFailoverRejected instead. See Planned failover outcomes.
PlannedFailoverPromotionConfirmationFailedWarningsuper_read_only=OFF may have reached the target but the promotion could not be confirmed. Check which site is writable.
PlannedFailoverStalledManagerAbsentWarningA planned failover that holds its claim has had no topology manager for 30 seconds; it ends after 5 minutes.
PlannedFailoverStatusStalledWarningA planned failover's status writes failed for 2 minutes and the run ended with the planned-failover-terminal annotation, or the operator cannot write the group at all.
PlannedFailoverLegacyRunRefusedWarningA planned-failover block written by an older operator was refused after an upgrade. A SourceLeftFenced refusal (the old run was draining or waiting for lag) or a PromotedUnconfirmed refusal (it was promoting or resuming, so the target may be writable) needs a human.
PlannedFailoverManualRecoveryRequiredWarningA planned failover may have written to its target and cannot explain the result. Automation is held (AutomaticFailoverHeld=True) until acknowledged. Page on it; see the hold runbook. Repeats the condition reason.
PlannedFailoverHoldPersistedWarningRepeated every 10 minutes while an automation hold stands.
PlannedFailoverHoldAcknowledgedNormalThe hold was acknowledged and cleared; automation resumes. Repeats the condition reason.
PlannedFailoverHoldAcknowledgmentIgnoredWarningThe acknowledge-planned-failover-hold annotation resumed nothing: it names a run other than the held one, no automation hold is standing for the run it names, or the planned-failover claim is held for another run. When no hold stands the annotation is removed anyway.

Deployment coordination

Recorded on MysqlFailoverGroup.

ReasonTypeDescription
DeploymentLeaseRevokedWarningThe group's deployment authority was revoked by a topology change (TopologyChanged) or an explicit operator request (OperatorRevoked). Stop the affected deployment clients; uncertain post-DDL state needs manual recovery. See deployment coordination.

Dragonfly

Recorded on MysqlFailoverGroup.

ReasonTypeDescription
DragonflyPromotionStartedNormalThe operator is promoting a Dragonfly replica: in a planned failover's PromotingDragonfly phase, because the MySQL active site differs from the Dragonfly master, or because the Dragonfly master is unreachable and exactly one healthy replica can take over.
DragonflyPromotionSucceededNormalA Dragonfly target was promoted with REPLTAKEOVER and the active Service now points at it. A promotion that fell back to REPLICAOF NO ONE emits DragonflySessionsLost instead.
DragonflyPromotionFailedWarningA Dragonfly target was not promoted: it was unreachable, REPLTAKEOVER failed or its reply was lost, it already reported master before the takeover, the Dragonfly master was neither the run's source nor its target, or PromotingDragonfly passed its deadline. The source's traffic label is restored; a planned failover then continues best-effort or rolls back, as onSyncTimeout says. Also emitted when the old master could not be demoted after a promotion: it is kept out of the active Service and needs manual intervention.
DragonflyPromotionDeferredWarningA planned failover reached PromotingDragonfly while another Dragonfly promotion, usually an emergency one, was still running. MySQL writes stay paused on the fenced source until it finishes, at most about 40 seconds. Emitted once per wait; status.plannedFailover.dragonfly.message says the same.
DragonflySyncTimedOutWarningA planned failover's target Dragonfly replica did not catch up within spec.dragonfly.plannedFailover.maxSyncWait. With onSyncTimeout: Proceed (the default) the takeover goes ahead and status.plannedFailover.dragonfly.sessions records Lost; with Fail the run rolls back before MySQL is promoted.
DragonflySessionsLostWarningAn emergency Dragonfly promotion succeeded without REPLTAKEOVER: it fell back to REPLICAOF NO ONE after the takeover failed or was skipped because the old master was unreachable, or its reply was lost and the target was then observed as master. The target is writable, but cached data and sessions may be lost.
DragonflyStaleMasterDetectedWarningA Dragonfly site other than the active one reports the master role. The operator reattaches it as a replica only if it never accepted writes (connected_slaves=0 and master_repl_offset=0); otherwise it stays out of the active Service until you decide whether its data can be discarded. Repeated at most every 5 minutes per site.
DragonflyOldSiteReconfiguredNormalA stale Dragonfly master on a non-active site that provably never accepted writes (connected_slaves=0 and master_repl_offset=0) was reattached as a replica of the active master.
DragonflyReplTakeoverSupportedNormalDragonfly advertises REPLTAKEOVER again after a DragonflyReplTakeoverUnsupported Warning, typically after an image change.
DragonflyReplTakeoverUnsupportedWarningA Dragonfly site does not advertise REPLTAKEOVER. Emergency promotions fall back to REPLICAOF NO ONE, which does not preserve session continuity. Emitted when the capability probe changes to unsupported; use a Dragonfly image that supports the command.
DragonflyUpgradeStartedNormalA snapshot-restore upgrade run started: spec.dragonfly.upgradeStrategy is SnapshotRestore and a site's live StatefulSet runs an image other than spec.dragonfly.image. status.dragonfly.upgrade records it; the message gives the source and target images. Dragonfly promotions and replication changes pause until the run ends.
DragonflyUpgradeSnapshotStartedNormalThe active Dragonfly site was removed from the active Service and the operator is saving a snapshot. Cache and session traffic is unavailable until the upgrade completes or fails.
DragonflyUpgradeSnapshotCompletedNormalSAVE completed on the active Dragonfly master into spec.dragonfly.snapshot.dir. The active pod is restarted on the target image next and restores from the snapshot.
DragonflyUpgradeSucceededNormalEvery Dragonfly site runs the target image and the active site serves traffic again with its replicas attached.
DragonflyUpgradeFailedWarningA Dragonfly snapshot-restore upgrade failed, once per run; the message gives the reason. On DragonflySnapshotDialFailed, DragonflySnapshotSaveFailed, UnknownPhase, WorkloadSelectorMismatch, ActiveWorkloadDeleted, SpecChanged, DragonflyDisabled or TimedOut the operator restores the active site's traffic label, except that an active the run restarted and that is still loading its snapshot keeps it off until it answers as loaded (the message says so); on ActiveWorkloadDeleted it re-creates the active site's StatefulSet. On ActiveSiteChanged the upgrade was abandoned because MySQL failed over or a promotion moved the Dragonfly master. SpecChanged: spec.dragonfly changed during the run, and the operator decides again for the new spec. TimedOut: the run had not finished an hour after it started. The run is not restarted while spec.dragonfly is unchanged: change it to retry. When every site still runs one image, setting spec.dragonfly.image back to it and then forward again also retries; when the sites run different images, that starts a run back to the old image instead.
DragonflyUpgradeDeferredNormalA snapshot-restore upgrade run is needed but waits: status.activeSite is empty, a site's StatefulSet is refused for its selector (WorkloadSelectorMismatch), a planned failover run or automation hold owns the sites, the Dragonfly master is not on the MySQL active site, or the sites do not run the other spec.dragonfly settings (snapshot flags, credentials, service account) yet. Emitted when the cause changes; the run is retried every 10 seconds and starts once the cause clears. No site's image moves meanwhile; the render rolls out the other settings, keeping each site's image.

Encryption at rest

Recorded on MysqlFailoverGroup.

ReasonTypeDescription
EncryptionAdoptionRefusedWarningspec.encryptionAtRest.enabled was turned on for a group that is already serving, with spec.encryptionAtRest.existingData: Refuse (the default). Existing tablespaces would stay plaintext, so EncryptionAtRestReady is False with AdoptionRefused. enabled cannot be turned back off. Restore a backup into a new encrypted group and move the applications to it, or set existingData: LeavePlaintext to accept partial coverage.
KeyringPhaseChangedNormalA site's keyring moved to another phase (Pending, Unsealed, Escrowed, Sealed or Failed); the message gives the old and new phase and the site's status message. A move to Failed comes with its own Warning, such as KeyringEscrowTimedOut or KeyringEscrowMissing.
KeyringUnsealedNormalA site was unsealed for a CLONE INSTANCE: its pod rolls onto a writable keyring so the clone can rewrap the donor's tablespace keys. The site is escrowed and sealed again after the clone.
KeyringEscrowTimedOutWarningA site's keyring was not escrowed within keyring.escrowTimeout (default 10m) of being unsealed, and the site goes Failed. The site stays unsealed and the operator keeps retrying; the message carries the last reason. See Keyring not sealed.
KeyringEscrowMissingWarningA sealed site's escrow Secret, its only copy of its keys, no longer exists, so the site cannot restart. The site goes Failed. Do not restart or delete the pod: recreate the Secret from it following Keyring escrow lost.
KeyringEscrowCorruptWarningA sealed site's escrow Secret, its only copy of its keys, no longer matches the recorded digest. The site goes Failed. Do not restart the pod; follow Keyring escrow lost.
KeyringDigestMismatchWarningA site's pod runs a keyring that does not match its escrow Secret, either when it was about to be declared Sealed or later, when the pod may be projecting a superseded version. The site goes Failed; tablespaces rewrapped under a newer master key will not decrypt. Check which escrow version the site's Deployment projects.
KeyringNotReadOnlyWarningMySQL reports a writable keyring component on a site the operator considers Sealed, so the site is not protected as reported. The site goes Failed; check the site's keyring rendering.
KeyringRecoveredNormalA Failed site returned to Sealed: its escrow Secret is present and matches the recorded digest, and MySQL confirms it runs that keyring.
KeyringRotationRejectedWarningA rotate-keyring request was refused. For the active primary the annotation is removed: rotate the replicas, run a planned failover, then rotate the former primary. Any other refusal (an ordered update, planned failover or promotion in flight, an unsettled topology, no running topology manager) keeps the annotation and retries every 10 seconds, with one Event per reason.
KeyringPromotionSkippedWarningEmergency failover skipped a site whose UnsealReason is Rotation and promoted another candidate.
KeyringPromotionRefusedWarningEmergency failover refused: every remaining candidate is mid-keyring-rotation. The group stays without a writable primary until the site is Sealed.

Restore

Recorded on MysqlFailoverGroup.

ReasonTypeDescription
RestoreStartedNormalRestore Job created.
RestoreSucceededNormalRestore completed (with or without PITR). Not emitted for a partial outcome.
RestoreFailedWarningRestore Job failed. Emitted once per Job; PITR failures carry a stable reason in status.restore.reason. For initFromBackup the message also states the recovery: the group stays out of service (no DNS record, no Service endpoints) and nothing is cloned. If the load had started, empty the target: delete and recreate the group, or delete the group's MySQL pods and PVCs together, wait until the operator has re-created every claim Bound and every pod Running, then delete the restore Job. Ready=False with reason InitFromBackupFailed says the same.
RestoreTargetUnavailableWarningThe active site an initFromBackup restore would load into is unreachable or in an unknown state; the restore waits in Pending.
RestoreBuildFailedWarningFailed to build restore Job spec. PITR render refusals (PITRArchiveDestinationShared when pointInTime.archiveProfileName resolves to the group's own active PITR destination, PITRTargetInvalid for an unparsable or fractional target, a missing or unknown archiveProfileName, or an excludeGtids entry without a server UUID or longer than 64 KiB canonical, PITRBackupIdentityUnknown for a backup with no recorded source identity (no stamp) or a direct S3/PVC source at the root of its store, PITRBackupFiltered for a filtered load) also set status.restore.reason. PITRBackupAfterTarget is not raised here: the restore Job checks the target against the dump itself. A cause inside spec.initFromBackup, which is fixed at creation, names the recreate remedy (delete the group and create it again with the corrected block); a restoreInPlace cause names the edit.

Point-in-time replay results

Recorded on MysqlFailoverGroup and MysqlBackupVerification.

ReasonTypeDescription
PITRReplayTruncatedWarningallowPartial replayed to the archive head because nothing is archived at or after the requested target (pointInTime.outcome: Partial: status.restore.provenance.pointInTime on a group, status.pointInTime on a verification). Names the requested target and the newest replayed commit time.
PITRDivergentHistoryExcludedWarningThe replay excluded N archived transactions committed before the target that are outside the target history (pointInTime.excludedDivergentTransactionCount).
PITROnHistoryTransactionsExcludedWarningAn excluded transaction on the target history precedes a replayed one: "excluded <set> precedes replayed transactions on the target history; their GTIDs were skipped but later row images may carry their effects; the result is not certified for them".

In-place restore

Recorded on MysqlFailoverGroup.

ReasonTypeDescription
RestoreInPlaceStartedNormalA new in-place restore was accepted and entered Preflight; the message gives the scope and confirm.
RestoreInPlaceJobCreatedNormalThe in-place restore Job was created against the active site; the run is Restoring.
RestoreInPlaceLoadedNormalThe restore Job succeeded and the load is complete; the operator lifts the fence (Resuming) and then marks the run Succeeded.
RestoreInPlaceSucceededNormalThe in-place restore finished Succeeded, and status.restoreInPlace.confirmTokenUsed records the confirm it ran with. After a full-instance restore the peers are recloned from the restored primary next.
RestoreInPlaceFailedWarningThe run's restore Job failed; the run ends Failed with the Job's failure in the message. The confirm the Job ran with is used (confirmTokenUsed), so bump confirm to retry. The fence and topology freeze are released; for the peers of a full-instance restore see RestoreReclonesHeld or RestoreReclonesDiscarded.
RestoreInPlaceRejectedWarningThe run ends Failed with nothing restored because the request was refused: the spec is invalid (for example more than one loadOptions.includeSchemas entry), the operator account lacks privileges a full-instance restore needs (the message names them; re-checked about once a minute), or the scope changed before the Job was created. The confirm is not consumed, so a fixed spec runs with the same confirm.
RestoreInPlaceBuildFailedWarningThe restore credentials Secret or the restore Job spec could not be built (for example a backupRef whose profile is gone or moved to another backend). The run ends Failed with nothing restored; the confirm counts as used, so bump confirm to retry.
RestoreInPlaceCanceledWarningspec.restoreInPlace was removed before the restore Job was created; the run ends Failed and nothing was restored.
RestoreInPlaceSpecRemovedWarningspec.restoreInPlace was removed while its restore Job runs; the fence and guards hold until the Job finishes.
RestoreInPlaceJobLostWarningThe run's restore Job disappeared before its outcome was seen; the run ends Failed with the outcome unknown.
RestoreInPlaceJobBusyWarningA Job with the in-place restore name from an earlier request is still running, so this run waits for it to finish instead of deleting a destructive restore in flight. Repeated until it finishes.
RestoreInPlaceStaleJobRemovedNormalA finished Job with the in-place restore name, left by an earlier request, was deleted so this run can create its own.
RestoreInPlaceConfirmReboundNormalspec.restoreInPlace.confirm advanced before the restore Job was created; the Job runs under the new confirm, recorded in status.restoreInPlace.runConfirm.
RestoreInPlaceConfirmDeferredWarningspec.restoreInPlace.confirm changed while the run's restore Job is running; the Job is never interrupted. A newer confirm starts its own run once this one is terminal, and one that does not advance past it starts nothing. Repeated while the Job runs.
RestoreInPlaceConfirmIgnoredWarningAfter an in-place restore ended, spec.restoreInPlace.confirm was changed to a value that is not an RFC 3339 timestamp, so no new run starts and the previous terminal status stays. Set confirm to a valid timestamp newer than status.restoreInPlace.confirmTokenUsed. Admission refuses such a value, so only an object stored before that rule reaches it.
RestoreInPlaceCredsRefreshFailedWarningThe restore credentials Secret could not be refreshed while the run's restore Job exists. The Job is left running; the refresh is retried on the next pass.
RestoreInPlaceDeferredPreviousQueueNormalA new in-place restore (bumped confirm) has not started because the previous restore's reclone queue is not settled yet: its Job's pods may still be running, or the primary is briefly unreadable (at most ten minutes). status.restoreInPlace still shows the previous run.
RestoreOfflineFenceAppliedNormalA full-instance in-place restore set offline_mode=ON on the primary and verified that no client session from before the fence remains; the restore Job is created next.
RestoreOfflineFenceSessionsKeptWarningThe operator account lacks SYSTEM_USER, so it could not kill the named SYSTEM_USER accounts' sessions when the fence went up. They stay connected during the restore.
RestoreOfflineFenceReleasedNormalThe restore finished and offline_mode on the primary is back to its prior value.
RestoreOfflineFenceReleaseFailedWarningThe operator could not put offline_mode back after the restore; it retries. Applications without CONNECTION_ADMIN cannot connect until it succeeds.
RestorePeerReplicationStoppedNormalA full-instance restore stopped the replication receiver of every queued peer in Fencing, before creating its Job.
RestorePeerReplicationStoppedBehindWarningA peer's receiver was stopped before it received every transaction the primary committed before the fence (it was connecting, or did not catch up within 30 s). The message names the missing GTIDs; the peer keeps its pre-restore data without them.
RestoreInPlacePeerStopFailedWarningThe receivers of the peers could not be verified stopped within two minutes (unreachable peer, refused STOP). The run ends Failed without a Job, nothing was restored, the confirm is consumed, and the peers resume.

Peer reclones after an in-place restore

Recorded on MysqlFailoverGroup.

ReasonTypeDescription
RestoreRecloneRequestedNormalThe in-place restore's reclone queue requested the reclone of its next peer.
RestoreRecloneWithdrawnNormalA newer full-instance restore was accepted; the old queue's reclone request that had not started was withdrawn.
RestoreRecloneSucceededNormalA queued peer was recloned from the restored primary and left the queue.
RestoreRecloneFailedWarningA queued peer's reclone failed, or did not start within ten minutes. The site stays queued; the other queued sites go first and it is retried after a minute.
RestoreReclonesDrainedNormalEvery peer of the in-place restore has been recloned; status.restoreReclones is cleared.
RestoreReclonesDiscardedWarningThe in-place restore that armed the queue did not succeed and left the primary unchanged: it never created its Job, or every pod of its Job terminated and the primary's gtid_executed still equals status.restoreReclones.fencedGtidExecuted. The peers resume replicating and are eligible for promotion again.
RestoreReclonesRestoredNormalA restore that had replaced an earlier restore's reclone queue ended without changing the primary. The earlier queue drains again.
RestoreReclonesHeldWarningA full-instance restore ran its Job on the primary, failed, and the primary changed (or could not be compared). Every peer, plus any site of a replaced earlier queue, is held: replication receiver stopped, pre-restore data kept, excluded from promotion and the reader Services, nothing cloned. An administrator restores the primary (for example from a backup of a held site), sets status.restoreReclones.phase to Draining, or removes the queue (which resumes their replication from the current primary).
RestoreReclonesAbandonedWarningA queued site became the primary (a manual promotion; a planned failover refuses a queued target) before it was recloned. It keeps its pre-restore data and the rest of the queue is dropped.

PITR archiving

Recorded on MysqlFailoverGroup.

The archive identity reasons are refusals, not transient errors: they do not clear by themselves. They are listed in Archive identity.

A PITRArchiveNotReady โ†’ PITRArchiveRecovered pair is expected, not an incident, when:

  • A pod takes more than 60 s to come back during a rollout or node move (SidecarUnreachable), for example while an RWO volume detaches and reattaches.
  • A new group's pods, or a bootstrapped site, take more than 5 minutes to start answering (SidecarUnreachable).
  • PITR is enabled with updateStrategy.type: Recreate, or a pod does not roll onto the PITR configuration through the ordered update (ArchiverDisabled). With the default ordered update, sites waiting for it report Unknown/AwaitingRollout with no Event, for up to 30 minutes; a rollout still pending after that is ArchiverDisabled, as is a PITR spec whose profileName renders no archiver.
  • MySQL takes more than about a minute to accept connections after a pod start, for example during a long InnoDB crash recovery (ArchiverError, role check: ...).

Two failed archiver scans in a row (about 2 minutes at the default 60 s scan interval) also produce a pair if the third scan succeeds. Alert on a PITRArchiveNotReady without a following PITRArchiveRecovered within, say, 10 minutes, or on the condition itself.

ReasonTypeDescription
PITRArchiveNotReadyWarningThe PITRArchiveReady condition entered False (from absent, Unknown or True). The message reads "PITRArchiveReady is False (): ", with the reason (ArchiveStoreInitFailed, an archive identity refusal such as ArchiveLegacy, ArchiveForeign, ArchiveCopy, ArchiveDescriptorMissing, ArchiveEncryptionMismatch, ArchiveKeyMismatch, ArchiveVersionUnsupported, ArchiveStoreUnsupported or ArchiveUnsafeHistory, then ArchiverError, SidecarOutdated, ArchiverDisabled, SidecarUnreachable) and every affected site. Not repeated while the condition stays False, even if the reason changes, and not re-fired by an operator restart.
PITRArchiveRecoveredNormalThe PITRArchiveReady condition returned from False to True after 2 clean poll rounds: "PITRArchiveReady is True again: every site reports a running archiver with a ready store and no error".
PITRArchiveUnsafeHistoryDetectedWarningEdge-triggered on entering PITRArchiveReady=False / ArchiveUnsafeHistory: a site persisted its history-integrity marker after an out-of-band change (SET GLOBAL, a hand-edited my.cnf): such options in spec.mysqlConf are refused instead (PITRMysqlConfRefused). Fires in addition to PITRArchiveNotReady. The marker removes every restore target that needs that site's bytes, including targets before the violation. Remedy: undo the change, new prefix plus full backup (Unsafe history marker).
PITRArchiveReissueRiskDetectedWarningThe active site changed to a site whose manifest holds divergent or quarantined rows. If it was recloned after a lossy failover it reissues the lost GTIDs; start a new archive prefix and take a full backup (Reissue risk after a reclone).
PITRMysqlConfRefusedWarningWhile PITR is enabled, group or site spec.mysqlConf sets an option that breaks PITR replay (a replication or binlog filter, binlog_formatโ‰ ROW, binlog_row_imageโ‰ FULL). The option is left out of the rendered my.cnf and PITRMysqlConfAccepted is False / PITRUnsafeSettingRefused; fires once per refused set. Remove the option from spec.mysqlConf.

PITR archive encryption bindings

Recorded on MysqlFailoverGroup.

Events for the group's archive encryption bindings (condition PITRArchiveBindingReady).

ReasonTypeDescription
PITRArchiveBoundNormalA binding became Bound by sidecar proof or a check Job. Not emitted when status-loss recovery rebuilds a binding; watch for the pitr archive binding changed log line with cause=recovery instead (log schema).
PITRArchiveBindingReleasedNormalA binding was released and its key snapshots deleted.
PITRArchiveEncryptionChangeRefusedWarningA profile in archive use has encryption that differs from its Bound binding. Pods are unchanged. Edge-triggered.
PITRArchiveKeyChangeRefusedWarningA profile in archive use has passphrase bytes that differ from its Bound binding's key snapshot (rotation, alias, rename while PITR was off). Pods are unchanged. Edge-triggered.
PITRArchiveBindingRefusedWarningA binding was refused (check Job or correlated sidecar refusal), a write target is not eligible (ArchiveForeign/ArchiveCopy), or site Deployments without a decodable render annotation are deferred until a check verifies (the message names only those: "Deployments <names> are not updated until the archive accepts a key: ..."). Check refusals name the role: "... reported by the writer check of ..." or "... reported by the reader check of ...".
PITRArchiveKeySourceRemovedWarningThe profile's Secret or key is gone; the snapshot still serves.
PITRArchiveKeySnapshotRecreatedWarningA deleted key snapshot was re-created under the same name from matching live bytes; no roll. Not emitted when another object of that name appeared first.
PITRArchiveKeyMaterialLostWarningA Bound binding's key snapshot is missing and cannot be reproduced ("Secret <s> held the accepted key of archive <id>: restore it from a backup of it, ..."), or is present but no longer passes the check of the accepted key ("... do not delete it, it may hold the only copy of that key; ..."). MySQL keeps running; the archiver fails closed.
PITRArchiveKeySnapshotReadFailedWarningA key snapshot's acceptance annotation is malformed; it is never used for recovery and never deleted.
PITRArchiveBindingReleaseRefusedWarningA release annotation names a binding still in use; retried every minute.
PITRArchiveBindingReleaseRejectedWarningA release annotation is malformed or its confirm is not the group name; the annotation is removed.
PITRArchiveBindingLimitReachedWarningA destination in use gets no binding because 16 entries that stay (Bound, or used by a target) exist, or status-loss recovery found more accepted snapshots than free slots.

Backup and verification scheduling

Recorded on MysqlFailoverGroup.

ReasonTypeDescription
BackupScheduleInvalidWarningSchedule references an unknown backup profile. Admission refuses such a schedule, so only an object stored before that rule reaches it.
BackupScheduleRefusedWarningThe schedule's profile is read-only (an unfinished initFromBackup archive profile, or a destination whose binding is foreign or refused ArchiveForeign/ArchiveCopy, or the archive profile of a succeeded initFromBackup restore whose destination has no binding): no CronJob is rendered. Once per change.
BackupRetryScheduledNormalRetry scheduled for a failed backup.
BackupRetryConflictWarningExisting deterministic retry name has different lineage; emitted when the schedule's retryConflict status marker changes. Only that retry is skipped while other schedules and rollups continue.
BackupEncryptionInvalidWarningAn encrypted backup profile's passphrase Secret is missing, lacks the configured key (default passphrase), holds an empty value, or holds a passphrase shorter than 32 bytes; the message names the profile and the problem. While the Secret or key is missing, backups of the profile wait Pending with PassphraseSecretMissing. A short passphrase only warns: backups keep using it, so replace it with a long random value.
BackupPITRInvalidWarningspec.backup.pitr.enabled is true but spec.backup.pitr.profileName is empty or names no entry in spec.backup.profiles. Binlog archiving stays disabled and PITRArchiveReady reports ArchiverDisabled; set profileName to an existing profile. Admission refuses both, so only an object stored before that rule reaches it.
VerificationScheduleInvalidWarningA backup profile sets verification.enabled: true but leaves verification.schedule empty, so no verification CronJob is created for it. Set a schedule or disable verification. Admission refuses it, so only an object stored before that rule reaches it.

Credentials and secrets

Recorded on MysqlFailoverGroup.

ReasonTypeDescription
CredentialReconcileFailedWarningFailed to reconcile MySQL users.
SecretNotFoundWarningReferenced Secret not found.
SecretMissingKeyWarningA credential Secret is missing a required key, has an empty username or password, sets only one of MYSQL_REPLICATION_USER and MYSQL_REPLICATION_PASSWORD, or names the operator account in MYSQL_REPLICATION_USER with a different MYSQL_REPLICATION_PASSWORD; the message names the Secret and key.
S3CredentialsIncompleteWarningA credentialsSecretName the group references for S3 storage is missing, or lacks a non-empty AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY; the message names each Secret and key. S3CredentialsReady is False, and backup, restore and cleanup Jobs fail because they do not fall back to ambient cloud credentials. Add both keys to the Secret. The initFromBackup.source.s3 Secret is checked only until the bootstrap restore succeeds; after that it can be deleted.

Operator lifecycle and license

Recorded on MysqlFailoverGroup.

ReasonTypeDescription
GracefulShutdownNormalGraceful shutdown started or completed.
LicenseVerifiedNormalA presented token newly verified offline. Also used when a valid token's update period has already ended (perpetual state).
LicenseInvalidWarningA presented token failed offline verification. Operator behavior is unchanged.

Backup lifecycle

Recorded on MysqlBackup.

ReasonTypeDescription
BackupStartedNormalBackup Job created.
BackupSucceededNormalBackup completed successfully.
BackupFailedWarningBackup Job failed, or it succeeded at a destination other than the recorded status.artifactStorage (Ready=False/DestinationMismatch).
BackupMetadataIncompleteWarningCompletion logs are unavailable, missing, or disagree with the Job. Check MetadataComplete; a recovered location remains usable, while DestinationUnavailable keeps Ready=False.
EncryptionAttributionCorrectedWarningBackup encryption status repaired from its producing Job's launch configuration.
InFlightFailoverWarningActive site changed while a backup was in progress.
PITRArchiveStampSkippedNormalThe backup was created with no PITR archive stamp (status.pitrArchiveID empty), so it is no point-in-time baseline: the site renders disagree or one is unreadable (a backup of a profile other than the active PITR profile, which does not wait for the roll), or the render is deferred (any site Deployment lacks a decodable render while the write binding is not Bound/Establishing: the pre-v1.7.0 deferral, which a backup of the active profile does not wait for). The message says why; when the renders disagree it also names Deployments of sites no longer in spec.sites ("... belong to sites no longer in spec.sites and are never re-rendered: scale them to zero or delete them").
RetentionDeferredNormalRetention kept a backup past its window because a verification is reading it.
BackupJobConflictWarningA Job named mysqlbackup-<backup> exists but is not owned by this MysqlBackup (a Job orphaned by an earlier backup of the same name, or an older operator's cleanup Job for a backup named <backup> minus -cleanup). The backup waits in Pending with Ready=False/BackupJobConflict, never adopts that Job, and fails with PendingTimedOut after 30 minutes if it is still there. Emitted once on entering the wait. Repeats the condition reason.
WaitingForHealthySourceWarningNo site qualifies as the backup's source (unreachable, not replicating, or lagging beyond maxLagSecondsForSource). The backup waits in Pending with Ready=False/WaitingForHealthySource and retries. Emitted once on entering the wait. Repeats the condition reason.
NoHealthySourceWarningTerminal: no healthy source appeared within 30 minutes, or a pinned spec.sourceSite cannot be used. The backup is Failed with Ready=False/NoHealthySource. Repeats the condition reason.
PendingTimedOutWarningTerminal: the backup waited longer than 30 minutes for anything other than a healthy source. The backup is Failed with Ready=False/PendingTimedOut; the message reads pending for <duration> (<pending reason>): <message>. Repeats the condition reason.
ProfileReadOnlyWarningThe backup's profile is read-only for this group (the initFromBackup archive profile before the restore succeeds or without a binding, or a destination whose binding names another group or was refused ArchiveForeign/ArchiveCopy). The backup is Failed with Ready=False/ProfileReadOnly and writes nothing. Repeats the condition reason.
PITRArchiveRolloutPendingWarningA backup of the active PITR profile waits while the site Deployments roll onto a new archive render, so it can become that archive's baseline. The backup waits in Pending with Ready=False/PITRArchiveRolloutPending and fails with PendingTimedOut after 30 minutes. Emitted once on entering the wait. Repeats the condition reason.
DumpKeySnapshotConflictWarningThe Secret the encrypted backup would use as its dump-key snapshot (mysqlbackup-<backup>-dump-key-<uid>) exists but is not labelled as this backup's snapshot. The backup waits in Pending with Ready=False/DumpKeySnapshotConflict and fails with PendingTimedOut after 30 minutes; delete or rename that Secret. Emitted once on entering the wait. Repeats the condition reason.
PassphraseSecretMissingWarningThe encrypted profile's encryption.passphraseSecretKeyRef Secret is missing, or its key is missing or empty, so no dump-key snapshot can be taken. The backup waits in Pending with Ready=False/PassphraseSecretMissing and fails with PendingTimedOut after 30 minutes. Emitted once on entering the wait. Repeats the condition reason.
BackupStorageMismatchWarningTerminal: the profile's owned claim mysql-<group>-backup-<profile> has another StorageClass than pvc.provisioned.storageClassName, or requests less than pvc.provisioned.size (the claim was created for an earlier profile of the same name). The backup is Failed with Ready=False/BackupStorageMismatch and never writes to that claim; delete the claim or restore the profile's values. Repeats the condition reason.
BackupStorageExpansionAcceptedNormalThe profile's owned claim requests more than pvc.provisioned.size, for example after it was expanded out of band. The backup uses it. Emitted when a backup's Job is created.

Backup and verification setup

Recorded on MysqlBackup and MysqlBackupVerification.

ReasonTypeDescription
GroupNotFoundWarningThe referenced MysqlFailoverGroup does not exist. The MysqlBackup is Failed with Ready=False/GroupNotFound, or the MysqlBackupVerification is Failed with Verified=False/GroupNotFound. Repeats the condition reason.
ProfileNotFoundWarningThe named backup profile is not in the group's spec.backup.profiles. A MysqlBackup without a Job yet is Failed with Ready=False/ProfileNotFound; a MysqlBackupVerification is Failed with Verified=False/ProfileNotFound. Repeats the condition reason.
BuildJobFailedWarningThe operator could not build the Job spec. On a MysqlBackup the backup is Failed with Ready=False/BuildJobFailed, also when the backup Job is gone mid-run and a replacement built from the current profile would not write to the destination recorded at launch. On a MysqlBackupVerification the run is Failed with Verified=False/BuildJobFailed; a PITR refusal uses its refusal token as the reason instead. Repeats the condition reason.

Artifact cleanup

Recorded on MysqlBackup.

ReasonTypeDescription
ArtifactCleanupStartedNormalCleanup Job created for backup artifact.
ArtifactCleanupSucceededNormalArtifact removed; the message gives the object count.
ArtifactCleanupNothingFoundWarningCleanup found nothing at the recorded destination; the finalizer is released.
ArtifactCleanupFailedWarningA cleanup Job failed, once per Job. The ArtifactCleanup condition names the cause; a new Job runs after 5 minutes (transient) or 30 minutes (refusal), and the finalizer blocks deletion until one succeeds. Counted by bloodraven_backup_artifact_cleanup_failures_total.
ArtifactCleanupRetryNormalA failed cleanup Job is being replaced.
ArtifactCleanupDeferredNormalA MysqlBackupVerification is still reading the artifact (ArtifactCleanup=False/HeldByVerification); cleanup waits until its pods stop.
ArtifactCleanupStalledProducerStopWarningDeleting an abandoned backup (never succeeded, or its group or profile is gone) waits for its producing backup Job's pods to stop, and one has not stopped after 10 minutes (ArtifactCleanup=False/ProducerStopStuck). The deletion stays held; once the pod's node is confirmed down, force-delete the pod (kubectl delete pod --force --grace-period=0).
ArtifactCleanupBlockedWarningCleanup cannot start (DestinationMismatch, CredentialsUnavailable, OperatorImageUnset, CleanupJobConflict, CleanupJobInvalid); the message carries the reason.
ArtifactCleanupSkippedWarningReferenced failover group or profile is gone, or no artifact destination can be recovered; or the best-effort cleanup of a backup that never succeeded (its stamp and any partial dump) could not run or failed, or found no stamp but objects at the location (BLOODRAVEN_CLEANUP_SKIPPED; nothing is deleted). Cleanup is skipped and the finalizer is released; the message names what is left (for the skipped verdict, the location, the stamp key and the kept dump-key snapshot). An encrypted backup that recorded neither its producing Job nor its artifact destination, or whose abandoned cleanup was skipped, keeps its dump-key snapshot (โ€ฆ is kept because encrypted artifacts may exist; delete it once you have confirmed none remain).
ArtifactRetainedNormalThe backup was deleted with spec.deletionPolicy: Retain: no cleanup ran, and the artifact and its dump-key snapshot Secret (an encrypted backup's key) are left in place; the message names both. The finalizer is released at once, or for a backup that never succeeded, once its producing Job's pods have stopped. Delete them by hand once they are no longer needed.

Backup verification

Recorded on MysqlBackupVerification.

ReasonTypeDescription
VerificationStartedNormalThe verification Job was created and the run moved to Restoring; the message names the Job, backup and profile.
VerificationSucceededNormalThe verification run reached Succeeded.
VerificationFailedWarningThe verification run reached Failed. A PITR failure's Verified condition reason is the refusal token, also when building the Job was refused (for example PITRTargetInvalid for a verification against a group with spec.backup.pitr disabled), not BuildJobFailed.
BackupIdentityMismatchWarningA verification's recorded source cannot be reconciled with its Job inputs. The run fails without certifying a different backup. Repeats the condition reason.
BackupNotAvailableWarningThe verification has no usable backup: the backupRef backup is missing, not Succeeded, being deleted or for another group or profile, no Succeeded backup exists for the group and profile, or the pinned backup was deleted, replaced by a same-name backup or lost this verification's hold. The run is Failed with Verified=False/BackupNotAvailable; create a new verification. Repeats the condition reason.
BlockedByActiveVerificationWarningAn older verification for the same group and profile has not finished, so this one is refused rather than run concurrently. The run is Failed with Verified=False/BlockedByActiveVerification. Repeats the condition reason.

PITR verification refusals

Recorded on MysqlBackupVerification.

ReasonTypeDescription
PITRArchiveBindingNotReady, PITRArchiveCopy, PITRArchiveDescriptorMissing, PITRArchiveDestinationShared, PITRArchiveDivergentHistory, PITRArchiveEncryptionMismatch, PITRArchiveForeign, PITRArchiveGap, PITRArchiveHistoryReset, PITRArchiveIdentityMismatch, PITRArchiveKeyMismatch, PITRArchiveLegacy, PITRArchiveListFailed, PITRArchiveNamespaceReuse, PITRArchiveNotFound, PITRArchiveObjectInvalid, PITRArchiveOrderConflict, PITRArchiveOrderInversion, PITRArchiveSplitBrain, PITRArchiveStoreUnsupported, PITRArchiveUnsafeHistory, PITRArchiveVersionUnsupported, PITRBackupAfterTarget, PITRBackupDumpKeyMissing, PITRBackupFiltered, PITRBackupIdentityMismatch, PITRBackupIdentityUnknown, PITRBackupIncomplete, PITRBackupInconsistent, PITRBackupNotOnLineage, PITRCoverageInvalid, PITRExcludeIntersectsBackup, PITRNoArchivedReplay, PITRPlanTooLarge, PITRReplayFailed, PITRReplayMismatch, PITRReplayUnsupported, PITRStopAmbiguous, PITRTargetInvalid, PITRTargetNotCoveredWarningA point-in-time verification was refused: the Event repeats the Verified=False reason, the refusal token. The tokens are listed in Restore refusals; PITRReplayFailed is the replay script's own failure. Each repeats the condition reason.

MysqlDatabase

Recorded on MysqlDatabase.

See Tenant databases for the lifecycle these Events report.

ReasonTypeDescription
DatabaseDroppedNormalUnder deletionPolicy: Delete the operator dropped the database and the CR's accounts, only the database (owner left untouched), or only the accounts (the database is still declared by another CR); the message says which. The finalizer is released.
DatabaseDropSkippedNormalUnder deletionPolicy: Delete this CR never applied any DDL (status.databaseCreated is unset), so there is nothing of its own to drop. The finalizer is released and MySQL is untouched.
DatabaseDropRefusedWarningUnder deletionPolicy: Delete another live MysqlDatabase on the group still declares the same databaseName, so the database is not dropped. The CR's own accounts are still vetted and dropped separately.
DatabaseDropDeferredWarningUnder deletionPolicy: Delete the DROP cannot run yet: the group has no active site, its primary is fenced by a restore or planned failover, a group credential Secret cannot be read, the primary is unreachable, a transient MySQL error hit, or the active site moved during the DROP. The finalizer stays and the drop is retried; set deletionPolicy: Retain to release the CR without dropping.
DatabaseRetainedNormaldeletionPolicy: Retain: the finalizer is removed and the database, the owner and the users[] principals are left untouched in MySQL.
DatabaseCleanupSkippedWarningUnder deletionPolicy: Delete the MysqlFailoverGroup is gone (before the DROP, or it disappeared during it), so there is nothing to connect to. The finalizer is released; the database and accounts may remain in MySQL.
OwnerUserRotatedNormalThe username in the owner Secret changed: the new owner user was created and granted, then the previous one was revoked and dropped. Also emitted when an abandoned rotation target is dropped before rotating to a newer username.
OwnerHostsRemovedNormalHosts were removed from spec.owner.hosts; the owner user was revoked and dropped on exactly those hosts.
OwnerUserDropSkippedWarningAn owner account was not dropped because something else still claims it: this CR's own spec.grants[], or another live MysqlDatabase (its owner Secret or user, its spec.grants[], or its users[] ledger). Emitted on deletion under deletionPolicy: Delete, and on an owner rotation or host removal, where the account still loses its rights on this database unless this CR's own spec.grants[] declares it.
OwnerUserReservedSkippedWarningAn owner account this CR recorded has since become a group-level principal, so it is never dropped. Emitted on deletion under deletionPolicy: Delete, and on an owner rotation or host removal, where its rights on this database are still revoked.
UserRotatedNormalThe username in a spec.users[] Secret changed: the new principal was created and granted, then the previous one was revoked and dropped. Also emitted when an abandoned rotation target is dropped before rotating to a newer username.
UserHostsRemovedNormalHosts were removed from a spec.users[] entry's hosts; that principal was revoked and dropped on exactly those hosts.
UserRemovedNormalA spec.users[] entry was removed; its principal was revoked and dropped on the hosts recorded for it.
UserTransferredNormalA spec.users[] entry was removed, but its principal is now declared by another surface of this CR (another users[] entry, the owner or spec.grants[]), so the account is kept and no UserRemoved follows.
UserDropSkippedWarningA users[] principal was not dropped because this CR's own spec.grants[] declares it or another live MysqlDatabase still claims it. Emitted on deletion under deletionPolicy: Delete, and on a rotation, entry removal or host removal, where the principal still loses its rights on this database unless this CR's own spec.grants[] declares it.
UserReservedSkippedWarningA users[] principal this CR recorded has since become a group-level principal, so it is never dropped. Emitted on deletion under deletionPolicy: Delete, and on a rotation, entry removal or host removal, where its rights on this database are still revoked.
PartialRevokeLiftedNormalThe operator removed a restriction on an impossible legacy escaped spelling of the database name from an account this CR recorded; such restrictions were left by older operator builds.
TenantPrivilegeRestrictionWarningThe best-effort repair of legacy escaped-schema restrictions found something it will not or could not fix: the operator account is restricted on the database (repairs on that spelling are skipped for every account), a recorded tenant account has a restriction on the real schema that needs review by the privilege owner, or a repair step failed. Normal tenant reconciliation continues; see the privilege restriction recovery playbook.
InvalidSpecWarningThe spec failed validation at reconcile time (for example an identifier or a username from a Secret that does not match the allowed pattern, or a MySQL system schema as databaseName). The CR is Failed with Ready=False/InvalidSpec and no SQL runs. Repeats the condition reason.
MySQLErrorWarningMySQL refused a statement with a non-transient error while applying the database, its accounts or grants, or while revoking or dropping a retired account. The CR is Failed with Ready=False/MySQLError and the message carries the error; retried every minute. Repeats the condition reason.
DatabaseNameConflictWarningAn older MysqlDatabase on the same group already declares this databaseName. This CR is Failed with Ready=False/DatabaseNameConflict before any SQL runs, and re-checked every minute. Repeats the condition reason.
DatabasePreExistsWarningThe schema already exists and status.databaseCreated does not say this CR created it, so adoption is refused. The CR is Failed with Ready=False/DatabasePreExists; re-checked every minute. Repeats the condition reason.
OwnerConflictWarningAn older MysqlDatabase on the same group shares this CR's owner Secret or owner MySQL user. This CR is Failed with Ready=False/OwnerConflict before any SQL runs; give each MysqlDatabase a distinct owner. Repeats the condition reason.
OwnerUserReservedWarningThe owner Secret names a group-level principal (root, replicator, a MySQL system account or a spec.credentials user). The CR is Failed with Ready=False/OwnerUserReserved and no SQL runs; use a tenant-specific username. Repeats the condition reason.
PreExistingOwnerUserWarningAn owner account user@host already exists and no record of this CR names that user on that host, so adoption is refused. The CR is Failed with Ready=False/PreExistingOwnerUser; re-checked every minute. Repeats the condition reason.
PreExistingUserWarningA spec.users[] account user@host already exists and no record of this CR names it on that host, so adoption is refused. The CR is Failed with Ready=False/PreExistingUser; re-checked every minute. Repeats the condition reason.
UserClaimedBySiblingWarningA spec.users[] username is already claimed by another MysqlDatabase on the group (as its owner, in its spec.grants[], or in its users[] ledger). The CR is Failed with Ready=False/UserClaimedBySibling before any SQL runs; share a group-level principal through spec.grants[] instead. Repeats the condition reason.
UserReservedWarningA spec.users[] Secret names a group-level principal (root, replicator, a MySQL system account or a spec.credentials user). The CR is Failed with Ready=False/UserReserved and no SQL runs. Repeats the condition reason.
GrantUserMissingWarningA spec.grants[] user does not exist at host %; the operator never creates it. The CR is Failed with Ready=False/GrantUserMissing; create the principal at the group level and the CR goes Ready on its own (re-checked every minute). Repeats the condition reason.
PartialRevokesOffWarningThe operator cannot confirm or enable partial_revokes=ON on the primary, so tenant changes are deferred. The CR is Pending with Ready=False/PartialRevokesOff and retries; existing accounts and grants are left intact. Repeats the condition reason.

MysqlStandbyCluster

Recorded on MysqlStandbyCluster.

ReasonTypeDescription
BucketScannedNormalA discovery scan changed status.discovered (the newest dump, the archive identity or its binlog window). The message gives the dump and the manifest and binlog counts. Rescans that find nothing new emit nothing.
ManifestParseFailedWarningA binlog manifest under the source prefix could not be read or parsed. The source archive is refused: SourceConfigKnown=False with the archive reason (for example ArchiveLegacy, ArchiveVersionUnsupported or ArchiveUnreadable), and latestDump is published without binlogArchive.
MetadataParseFailedWarningThe newest dump's @.json metadata is unreadable or malformed. SourceConfigKnown is False with reason MetadataUnreadable; the bucket itself stays readable.
PITRArchiveRefusedWarningOn a MysqlStandbyCluster: the source archive was refused (SourceConfigKnown=False with an archive reason); emitted when the reason changes.
AuthFailedWarningThe archive store for the source bucket could not be constructed (for example bad credentials or an unusable endpoint). BucketReadable and SourceConfigKnown are False with reason AuthFailed; discovery retries every discovery interval and the Event repeats on each failed attempt. Repeats the condition reason.
ConfigErrorWarningThe standby's storage backend could not be resolved: an invalid spec.source.s3 bucket, prefix or endpoint (a prefix must name more than the bucket root), an unreadable credentialsSecretName Secret, or a spec.source.decryption.passphraseSecretKeyRef Secret or key that is missing or empty. BucketReadable and SourceConfigKnown are False with reason ConfigError; retried every discovery interval. Repeats the condition reason.
ListFailedWarningListing the source bucket prefix failed. BucketReadable is False with reason ListFailed and status.discovered keeps the last successful scan; retried every discovery interval. Repeats the condition reason.

Forwarding events with Kubewatch

Kubewatch can watch for Kubernetes Events and forward them to Slack, PagerDuty, webhooks, and more. Example configuration to forward all Bloodraven events to Slack:

apiVersion: v1
kind: ConfigMap
metadata:
  name: kubewatch
data:
  .kubewatch.yaml: |
    handler:
      slack:
        channel: "#mysql-alerts"
    resource:
      event: true
    namespaces:
      - bloodraven
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kubewatch
spec:
  template:
    spec:
      containers:
        - name: kubewatch
          env:
            - name: KW_SLACK_TOKEN
              valueFrom:
                secretKeyRef:
                  name: kubewatch-secrets
                  key: slack-token
Store credentials in a Kubernetes Secret, not in the ConfigMap. The KW_SLACK_TOKEN environment variable is read by Kubewatch at startup.

Argo Events and Event Router are alternatives that offer richer filtering and routing capabilities.

Status API

The operator serves a JSON status API on :8082.

GET /status

Returns the current state of all failover groups:

curl http://localhost:8082/status
{
  "default/orders": {
    "activeSite": "iad",
    "sites": [
      {
        "name": "iad",
        "role": "PrimaryCandidate",
        "state": "Writable"
      },
      {
        "name": "pdx",
        "role": "PrimaryCandidate",
        "state": "ReadOnly"
      }
    ],
    "pollTime": "2025-01-01T00:00:00Z"
  }
}

GET /active-site

Returns the active (writable) site for a specific failover group. Used by the sidecar's startup safety net.

curl "http://localhost:8082/active-site?namespace=default&group=orders"
{
  "namespace": "default",
  "group": "orders",
  "activeSite": "iad"
}
StatusMeaning
200Failover group found. activeSite may be "" if no single writable site exists (first boot, split-brain).
400Missing namespace or group query parameter.
404Failover group not found on this operator instance.
503Operator has no active topology managers (startup race or non-leader replica).

GET /ws/status

WebSocket endpoint that streams the full topology of each failover group in real time. A message is sent at the end of every operator poll cycle, not only on state transitions, so dashboards can render live counters and health indicators. The cadence is every 2 seconds per group (pollInterval), including during a site outage: probe backoff is per site, so a site that stays down is re-probed less often, but the poll loop and this stream keep the base cadence.

Each message is a JSON object with camelCase keys:

{
  "namespace": "default",
  "group": "orders",
  "activeSite": "iad",
  "sites": [
    {
      "name": "iad",
      "role": "PrimaryCandidate",
      "state": "Writable",
      "lastSeenTime": "2026-04-10T00:00:00Z",
      "replication": "NotApplicable"
    },
    {
      "name": "pdx",
      "role": "PrimaryCandidate",
      "state": "ReadOnly",
      "lastSeenTime": "2026-04-10T00:00:00Z",
      "replication": "Healthy",
      "secondsBehindSource": 0,
      "gtidExecuted": "3e11fa47-71ca-11e1-9e33-c80aa9429562:1-45839"
    }
  ],
  "lastFailover": {
    "time": "2026-04-09T22:10:00Z",
    "target": "iad",
    "promotionGtidExecuted": "3e11fa47-71ca-11e1-9e33-c80aa9429562:1-45839"
  },
  "pollTime": "2026-04-10T00:00:00Z"
}

The top level carries namespace, group, activeSite, sites, lastFailover, alert, updatePhase and pollTime. lastFailover has the shape of the CR's status.lastFailover (time in RFC 3339, target, promotionGtidExecuted) and is omitted when the group has no failover record. Each site entry carries name, role, state, lastSeenTime, replication, secondsBehindSource, gtidExecuted, recoveryState, divergentGtid and divergentTransactionCount.

sites[].replication is the CR's status.sites[].replication value (Healthy, Lagging, Broken, Unknown or NotApplicable), debounced the same way: Healthy or Lagging only after both replication threads have run with a source set for consecutive polls, and NotApplicable on the writable primary. It is not a per-poll reading of the replication threads. The former boolean replicating field (the raw per-poll IO and SQL thread running flag) and the top-level lastFailoverTarget and promotionGtidExecuted fields are gone; lastSeen is now lastSeenTime.

When a site holds transactions the primary lacks (an old primary after an emergency failover, or a replica found by the replica GTID audit), the site entry includes recovery fields:

{
  "name": "pdx",
  "role": "PrimaryCandidate",
  "state": "ReadOnly",
  "lastSeenTime": "2026-04-10T00:00:00Z",
  "replication": "Broken",
  "recoveryState": "RecoveryBlocked",
  "divergentGtid": "a1b2c3d4-0000-0000-0000-000000000000:11-15",
  "divergentTransactionCount": 5
}

The stream does not say which check found the divergence. GET /status and the CR's status.sites[] carry divergenceDetectedBy (OldPrimaryRecovery or ReplicaAudit) and divergenceComparedTo; a replica-audit entry usually has replication: Healthy.

const ws = new WebSocket("ws://localhost:8082/ws/status");
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  console.log(`${msg.group}: active=${msg.activeSite}`);
};

Both the REST and WebSocket endpoints use camelCase JSON keys, so the same field names work for either consumer.

Troubleshooting

Common conditions

SymptomLikely causeInvestigation
Ready=False (reason NoWritablePrimary or ReplicationUnhealthy), Degraded=TrueA site is unreachable (SiteUnhealthy) or replication is brokenCheck status.sites[].state and MySQL pod logs
Degraded=True, reason BootstrapFailed; Bootstrapping=False, reason BootstrapFailedA clone into a site failed and the site does not replicate yet. Deployment leases are refused while it holdsRead the condition message for the target and the error, fix the cause (often replication credentials or the donor), then clone again with kubectl bloodraven reclone <group> <site> --cold. It clears once the site replicates or a new clone starts
Ready=Unknown, Degraded=Unknown, reason InitializingThe operator just started and has not classified every PrimaryCandidate site yet (a writable answer needs recoveryThreshold polls)Normally clears on the next poll. It lasts at most failureThreshold + recoveryThreshold probes of a site; a site still unclassified then alternates between answers and errors, and the group reports NoPrimary naming it. Check status.sites[].state and the operator logs. See Failover: Initializing after an operator start
Ready=False, reason InitFromBackupInProgressA new group's spec.initFromBackup restore has not succeeded yet. The seed is writable and status.activeSite, but applications are kept off it: no DNS record, no -primary/-replicas/per-site Service endpoints, no clonesWatch status.restore. See Backup and restore: Applications wait for the restore
Ready=False, reason InitFromBackupFailedThe initFromBackup restore failed; the seed may hold partial data. The group stays out of service and nothing is clonedFollow the condition message: read the restore Job log; if the load had started, empty the target (delete and recreate the group, or delete the group's MySQL pods and PVCs together and wait until the re-created claims are Bound and the pods Running), then delete the Job. See Backup and restore: A failed bootstrap restore
Ready=False, both sites ReadOnlyNo primary exists (may follow a failed failover)See Operations: Manual promotion
Ready=False, both sites WritableSplit brainSee Operations: Split brain recovery
RecoveryPending=True, reason RecoveryInProgressOld primary is being reconfigured as a replicaWait for the condition to clear. If it stays true, check operator logs and status.sites[].replication. See Failover: Old primary recovery.
RecoveryPending=True, reason DivergentTransactionsOld primary returned with divergent transactionsCheck status.sites[].divergentGtid, then trigger a reclone: kubectl bloodraven reclone <group> <site> (it fills in the required divergentGtid prefix). See Failover: Old primary recovery.
Degraded=True, reason DivergentTransactions, Ready=TrueA site holds transactions the primary lacks; a replica can be replicating normally. Ordered updates and planned failovers will not promote itCheck status.sites[].divergentGtid and divergenceDetectedBy, preserve the errant binlog events, then reclone the site or make the primary contain the set. See Failover: Divergent replicas.
Updating=True, reason OrderedUpdateStalledAn ordered update rolled a site whose new pod does not become Ready and stay Ready for 30s (often a sidecar that cannot start or crash-loops), or a site already on the new template is not Ready by that rule. The update rolls nothing else and the active site keeps servingRead the condition message for the site, pod and cause, then kubectl describe pod and the container logs (--previous for a crash loop). Fix the cause or the spec; the update retries every 30s. See Failover: Ordered updates.
DivergentPromotion=TrueA divergent site was promoted; its extra transactions are now authoritativeReview the set in the condition message, then remove the condition. See Failover: Failover sequence.
Repeated failoversFlapping network or unstable MySQLCheck bloodraven_state_transitions_total and consider increasing failureThreshold or failoverCooldown
High replication lagSlow network, heavy write load, or undersized replicaCheck bloodraven_replication_lag_seconds and MySQL performance metrics

Operator logs

The operator logs structured JSON. Key fields to filter on:

kubectl logs -n bloodraven deploy/bloodraven | jq 'select(.msg == "failover")'
kubectl logs -n bloodraven deploy/bloodraven | jq 'select(.level == "ERROR")'

Sidecar logs

Check the sidecar container for self-fencing events:

IAD_POD=$(kubectl get pod -n orders \
  -l app.kubernetes.io/name=mysql,bloodraven.shipstream.io/failover-group=orders,bloodraven.shipstream.io/site=iad \
  -o jsonpath='{.items[0].metadata.name}')
kubectl logs -n orders "$IAD_POD" -c sidecar | jq 'select(.msg | contains("self-fence"))'