use std::fmt;
use std::sync::atomic::AtomicU8;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use crate::config::ObservabilityConfig;
use crate::metrics::labels::MetricLabelPolicy;
#[cfg(feature = "otel-metrics")]
use crate::metrics::release_identity::ReleaseIdentityRegistration;
#[cfg(feature = "otel-metrics")]
use crate::metrics::release_identity::ReleaseIdentityRegistrationStatus;
#[cfg(feature = "otel-metrics")]
use crate::metrics::release_identity::ValidatedReleaseIdentity;
pub const BROKER_METER_SCOPE: &str = "rocketmq-broker";
pub const STORE_METER_SCOPE: &str = "rocketmq-store";
pub const CLIENT_METER_SCOPE: &str = "rocketmq-client";
pub const TRANSPORT_METER_SCOPE: &str = "rocketmq-transport";
pub const CONTROLLER_METER_SCOPE: &str = "rocketmq-controller";
pub const NAMESRV_METER_SCOPE: &str = "rocketmq-namesrv";
pub const PROXY_METER_SCOPE: &str = "rocketmq-proxy";
pub const MCP_METER_SCOPE: &str = "rocketmq-mcp";
pub const RUNTIME_METER_SCOPE: &str = "rocketmq-runtime";
pub const SRE_CONTROL_PLANE_METER_SCOPE: &str = "rocketmq-sre-control-plane";
pub const SRE_CONNECTOR_METER_SCOPE: &str = "rocketmq-sre-connector";
pub const TIERED_STORE_METER_SCOPE: &str = "rocketmq-tieredstore";
pub const DASHBOARD_METER_SCOPE: &str = "rocketmq-dashboard";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TelemetryState {
Active = 0,
Closing = 1,
Closed = 2,
}
impl TelemetryState {
fn from_raw(value: u8) -> Self {
match value {
value if value == Self::Active as u8 => Self::Active,
value if value == Self::Closing as u8 => Self::Closing,
_ => Self::Closed,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TracePolicy {
pub enabled: bool,
pub propagate_context: bool,
pub record_message_id: bool,
pub record_message_keys: bool,
pub record_body_size: bool,
}
impl TracePolicy {
fn from_config(config: &ObservabilityConfig) -> Self {
Self {
enabled: config.enabled && config.traces.enabled,
propagate_context: config.traces.propagate_context,
record_message_id: config.traces.record_message_id,
record_message_keys: config.traces.record_message_keys,
record_body_size: config.traces.record_body_size,
}
}
const fn disabled() -> Self {
Self {
enabled: false,
propagate_context: false,
record_message_id: false,
record_message_keys: false,
record_body_size: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MetricsRuntimePolicy {
pub enabled: bool,
pub sample_ratio: f64,
pub export_interval_millis: u64,
pub cardinality_limit: usize,
}
impl MetricsRuntimePolicy {
fn from_config(config: &ObservabilityConfig) -> Self {
Self {
enabled: config.enabled && config.metrics.enabled,
sample_ratio: config.metrics.sample_ratio,
export_interval_millis: config.metrics.export_interval_millis,
cardinality_limit: config.metrics.cardinality_limit,
}
}
const fn disabled() -> Self {
Self {
enabled: false,
sample_ratio: 0.0,
export_interval_millis: 0,
cardinality_limit: 0,
}
}
}
impl Default for MetricsRuntimePolicy {
fn default() -> Self {
Self::disabled()
}
}
#[derive(Clone)]
enum HandleBackend {
Noop,
Active(Arc<ActiveHandle>),
}
struct ActiveHandle {
state: AtomicU8,
metrics_runtime_policy: MetricsRuntimePolicy,
trace_policy: TracePolicy,
metric_label_policy: MetricLabelPolicy,
#[cfg(feature = "otel-metrics")]
meters: Option<FixedMeters>,
#[cfg(feature = "otel-metrics")]
release_identity: parking_lot::Mutex<Option<ReleaseIdentityRegistration>>,
}
#[cfg(feature = "otel-metrics")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ReleaseIdentityRegistrationError {
#[error("telemetry handle is not active")]
Inactive,
#[error("telemetry handle has no meter provider")]
MeterUnavailable,
#[error("release service does not match a fixed RocketMQ instrumentation scope")]
UnsupportedService,
#[error("telemetry handle already registered a different release identity")]
ConflictingIdentity,
}
#[derive(Clone)]
pub struct TelemetryHandle {
backend: HandleBackend,
}
impl fmt::Debug for TelemetryHandle {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TelemetryHandle")
.field("state", &self.state())
.field("trace_policy", &self.trace_policy())
.finish_non_exhaustive()
}
}
impl Default for TelemetryHandle {
fn default() -> Self {
Self::noop()
}
}
impl TelemetryHandle {
#[must_use]
pub const fn noop() -> Self {
Self {
backend: HandleBackend::Noop,
}
}
#[cfg(feature = "otel-metrics")]
pub(crate) fn active(
config: &ObservabilityConfig,
meter_provider: Option<&opentelemetry_sdk::metrics::SdkMeterProvider>,
) -> Self {
Self {
backend: HandleBackend::Active(Arc::new(ActiveHandle {
state: AtomicU8::new(TelemetryState::Active as u8),
metrics_runtime_policy: MetricsRuntimePolicy::from_config(config),
trace_policy: TracePolicy::from_config(config),
metric_label_policy: MetricLabelPolicy::new(
config.metrics.cardinality_limit,
config.metrics.topic_label_enabled,
config.metrics.consumer_group_label_enabled,
),
meters: meter_provider.map(FixedMeters::new),
release_identity: parking_lot::Mutex::new(None),
})),
}
}
#[cfg(not(feature = "otel-metrics"))]
pub(crate) fn active(config: &ObservabilityConfig) -> Self {
Self {
backend: HandleBackend::Active(Arc::new(ActiveHandle {
state: AtomicU8::new(TelemetryState::Active as u8),
metrics_runtime_policy: MetricsRuntimePolicy::from_config(config),
trace_policy: TracePolicy::from_config(config),
metric_label_policy: MetricLabelPolicy::new(
config.metrics.cardinality_limit,
config.metrics.topic_label_enabled,
config.metrics.consumer_group_label_enabled,
),
})),
}
}
#[must_use]
pub fn state(&self) -> TelemetryState {
match &self.backend {
HandleBackend::Noop => TelemetryState::Closed,
HandleBackend::Active(inner) => TelemetryState::from_raw(inner.state.load(Ordering::Acquire)),
}
}
#[must_use]
pub fn is_active(&self) -> bool {
self.state() == TelemetryState::Active
}
#[must_use]
pub fn metrics_enabled(&self) -> bool {
self.metrics_runtime_policy().enabled
}
#[must_use]
pub fn metrics_runtime_policy(&self) -> MetricsRuntimePolicy {
match &self.backend {
HandleBackend::Active(inner) if self.is_active() => inner.metrics_runtime_policy,
HandleBackend::Noop | HandleBackend::Active(_) => MetricsRuntimePolicy::disabled(),
}
}
#[must_use]
pub fn trace_policy(&self) -> TracePolicy {
match &self.backend {
HandleBackend::Active(inner) if self.is_active() => inner.trace_policy,
HandleBackend::Noop | HandleBackend::Active(_) => TracePolicy::disabled(),
}
}
#[must_use]
pub fn metric_label_policy(&self) -> MetricLabelPolicy {
match &self.backend {
HandleBackend::Active(inner) if self.is_active() => inner.metric_label_policy.clone(),
HandleBackend::Noop | HandleBackend::Active(_) => MetricLabelPolicy::disabled(),
}
}
#[must_use]
pub fn child(&self, scope: &'static str) -> TelemetryRecorder {
TelemetryRecorder {
handle: self.clone(),
scope,
}
}
#[cfg(feature = "otel-metrics")]
#[must_use]
pub(crate) fn meter(&self, scope: &str) -> Option<opentelemetry::metrics::Meter> {
match &self.backend {
HandleBackend::Active(inner) if self.is_active() => {
inner.meters.as_ref().and_then(|meters| meters.get(scope))
}
HandleBackend::Noop | HandleBackend::Active(_) => None,
}
}
#[cfg(feature = "otel-metrics")]
pub fn register_release_identity(
&self,
identity: ValidatedReleaseIdentity,
) -> Result<ReleaseIdentityRegistrationStatus, ReleaseIdentityRegistrationError> {
let HandleBackend::Active(inner) = &self.backend else {
return Err(ReleaseIdentityRegistrationError::Inactive);
};
let mut registration = inner.release_identity.lock();
if TelemetryState::from_raw(inner.state.load(Ordering::Acquire)) != TelemetryState::Active {
return Err(ReleaseIdentityRegistrationError::Inactive);
}
if let Some(existing) = registration.as_ref() {
return if existing.identity() == &identity {
Ok(ReleaseIdentityRegistrationStatus::AlreadyRegistered)
} else {
Err(ReleaseIdentityRegistrationError::ConflictingIdentity)
};
}
let meters = inner
.meters
.as_ref()
.ok_or(ReleaseIdentityRegistrationError::MeterUnavailable)?;
let meter = meters
.get(identity.service())
.ok_or(ReleaseIdentityRegistrationError::UnsupportedService)?;
let release_identity = ReleaseIdentityRegistration::new(identity);
let status = release_identity.register(&meter);
*registration = Some(release_identity);
Ok(status)
}
#[must_use]
pub fn release_identity_registered(&self) -> bool {
#[cfg(feature = "otel-metrics")]
{
let HandleBackend::Active(inner) = &self.backend else {
return false;
};
self.is_active()
&& inner
.release_identity
.lock()
.as_ref()
.is_some_and(ReleaseIdentityRegistration::is_registered)
}
#[cfg(not(feature = "otel-metrics"))]
{
false
}
}
pub(crate) fn begin_closing(&self) {
if let HandleBackend::Active(inner) = &self.backend {
#[cfg(feature = "otel-metrics")]
let _registration = inner.release_identity.lock();
let _ = inner.state.compare_exchange(
TelemetryState::Active as u8,
TelemetryState::Closing as u8,
Ordering::AcqRel,
Ordering::Acquire,
);
}
}
pub(crate) fn mark_closed(&self) {
if let HandleBackend::Active(inner) = &self.backend {
inner.state.store(TelemetryState::Closed as u8, Ordering::Release);
}
}
}
#[derive(Debug, Clone)]
pub struct TelemetryRecorder {
handle: TelemetryHandle,
scope: &'static str,
}
impl TelemetryRecorder {
#[must_use]
pub const fn scope(&self) -> &'static str {
self.scope
}
#[must_use]
pub fn is_active(&self) -> bool {
self.handle.is_active()
}
#[must_use]
pub fn trace_policy(&self) -> TracePolicy {
self.handle.trace_policy()
}
#[must_use]
pub fn metric_label_policy(&self) -> MetricLabelPolicy {
self.handle.metric_label_policy()
}
#[cfg(feature = "otel-metrics")]
#[must_use]
pub(crate) fn meter(&self) -> Option<opentelemetry::metrics::Meter> {
self.handle.meter(self.scope)
}
}
#[cfg(feature = "otel-metrics")]
#[derive(Clone)]
struct FixedMeters {
broker: opentelemetry::metrics::Meter,
store: opentelemetry::metrics::Meter,
client: opentelemetry::metrics::Meter,
transport: opentelemetry::metrics::Meter,
controller: opentelemetry::metrics::Meter,
namesrv: opentelemetry::metrics::Meter,
proxy: opentelemetry::metrics::Meter,
mcp: opentelemetry::metrics::Meter,
runtime: opentelemetry::metrics::Meter,
sre_control_plane: opentelemetry::metrics::Meter,
sre_connector: opentelemetry::metrics::Meter,
tiered_store: opentelemetry::metrics::Meter,
dashboard: opentelemetry::metrics::Meter,
}
#[cfg(feature = "otel-metrics")]
impl FixedMeters {
fn new(provider: &opentelemetry_sdk::metrics::SdkMeterProvider) -> Self {
use opentelemetry::metrics::MeterProvider;
Self {
broker: provider.meter(BROKER_METER_SCOPE),
store: provider.meter(STORE_METER_SCOPE),
client: provider.meter(CLIENT_METER_SCOPE),
transport: provider.meter(TRANSPORT_METER_SCOPE),
controller: provider.meter(CONTROLLER_METER_SCOPE),
namesrv: provider.meter(NAMESRV_METER_SCOPE),
proxy: provider.meter(PROXY_METER_SCOPE),
mcp: provider.meter(MCP_METER_SCOPE),
runtime: provider.meter(RUNTIME_METER_SCOPE),
sre_control_plane: provider.meter(SRE_CONTROL_PLANE_METER_SCOPE),
sre_connector: provider.meter(SRE_CONNECTOR_METER_SCOPE),
tiered_store: provider.meter(TIERED_STORE_METER_SCOPE),
dashboard: provider.meter(DASHBOARD_METER_SCOPE),
}
}
fn get(&self, scope: &str) -> Option<opentelemetry::metrics::Meter> {
match scope {
BROKER_METER_SCOPE => Some(self.broker.clone()),
STORE_METER_SCOPE => Some(self.store.clone()),
CLIENT_METER_SCOPE => Some(self.client.clone()),
TRANSPORT_METER_SCOPE => Some(self.transport.clone()),
CONTROLLER_METER_SCOPE => Some(self.controller.clone()),
NAMESRV_METER_SCOPE => Some(self.namesrv.clone()),
PROXY_METER_SCOPE => Some(self.proxy.clone()),
MCP_METER_SCOPE => Some(self.mcp.clone()),
RUNTIME_METER_SCOPE => Some(self.runtime.clone()),
SRE_CONTROL_PLANE_METER_SCOPE => Some(self.sre_control_plane.clone()),
SRE_CONNECTOR_METER_SCOPE => Some(self.sre_connector.clone()),
TIERED_STORE_METER_SCOPE => Some(self.tiered_store.clone()),
DASHBOARD_METER_SCOPE => Some(self.dashboard.clone()),
_ => None,
}
}
}