Skip to content

Commit 5352011

Browse files
committed
chore: ensure metrics are correctly exposed
1 parent 671c6c3 commit 5352011

3 files changed

Lines changed: 140 additions & 13 deletions

File tree

docs/modules/hbase/pages/usage-guide/monitoring.adoc

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,20 @@ See xref:operators:monitoring.adoc[] for more details.
66

77
Starting with HBase 2.6 the URL for Prometheus metrics has changed.
88
This is because HBase offers now a built-in endpoint for this purpose.
9-
This endpoint is available from the UI service.
10-
For example, in the case of the master service, the URL is `http://<master-service>:16010/prometheus`.
9+
This endpoint is available from the `metrics` Services.
10+
For example, in the case of the master Service, the URL is `http://<hbasecluster-name>-master-<rolegroup-name>-metrics:16010/prometheus`.
11+
12+
== Authentication when using TLS
13+
14+
HBase exposes metrics through the same port as their web UI. Hence, when configuring HBase with TLS the metrics are also secured by TLS,
15+
and the clients scraping the metrics endpoint need to authenticate against it. This could for example be accomplished by utilizing mTLS
16+
between Kubernetes Pods with the xref:home:secret-operator:index.adoc[Secret Operator].
17+
18+
When using Prometheus `ServiceMonitor` for scraping, the `address` label needs relabeling to use the `headless` Service instead of the
19+
`metrics` Service. This is because per default Prometheus targets the Pod IPs as endpoints, but since the Pod IPs are not
20+
part of the certificate, the authentication will fail. Instead, the FQDN of the Pods, which can be added to the certificate, is used, but
21+
this FQDN is only available through the `headless` Service.
22+
23+
A more detailed explanation can be found in the xref:home:nifi:usage_guide/monitoring.adoc[NiFi Operator Monitoring Docs] with a similar situation
24+
and an example of a Prometheus `ServiceMonitor` configured for TLS in the
25+
https://github.com/stackabletech/demos/blob/main/stacks/monitoring/prometheus-service-monitors.yaml[Monitoring Stack{external-link-icon}^].

rust/operator-binary/src/crd/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,20 @@ pub const HBASE_UI_PORT_NAME_HTTP: &str = "ui-http";
6969
pub const HBASE_UI_PORT_NAME_HTTPS: &str = "ui-https";
7070
pub const HBASE_REST_PORT_NAME_HTTP: &str = "rest-http";
7171
pub const HBASE_REST_PORT_NAME_HTTPS: &str = "rest-https";
72+
pub const HBASE_METRICS_PORT_NAME: &str = "metrics";
7273

7374
pub const HBASE_MASTER_PORT: u16 = 16000;
7475
// HBase always uses 16010, regardless of http or https. On 2024-01-17 we decided in Arch-meeting that we want to stick
7576
// the port numbers to what the product is doing, so we get the least surprise for users - even when this means we have
7677
// inconsistency between Stackable products.
7778
pub const HBASE_MASTER_UI_PORT: u16 = 16010;
79+
pub const HBASE_MASTER_METRICS_PORT: u16 = 16010;
7880
pub const HBASE_REGIONSERVER_PORT: u16 = 16020;
7981
pub const HBASE_REGIONSERVER_UI_PORT: u16 = 16030;
82+
pub const HBASE_REGIONSERVER_METRICS_PORT: u16 = 16030;
8083
pub const HBASE_REST_PORT: u16 = 8080;
8184
pub const HBASE_REST_UI_PORT: u16 = 8085;
85+
pub const HBASE_REST_METRICS_PORT: u16 = 8085;
8286
pub const LISTENER_VOLUME_NAME: &str = "listener";
8387
pub const LISTENER_VOLUME_DIR: &str = "/stackable/listener";
8488

@@ -542,6 +546,24 @@ impl v1alpha1::HbaseCluster {
542546
}
543547
}
544548

549+
/// Returns required metrics port name and metrics port number tuples depending on the role.
550+
/// The metrics are available over the UI port.
551+
pub fn metrics_ports(&self, role: &HbaseRole) -> Vec<(String, u16)> {
552+
match role {
553+
HbaseRole::Master => vec![(
554+
HBASE_METRICS_PORT_NAME.to_string(),
555+
HBASE_MASTER_METRICS_PORT,
556+
)],
557+
HbaseRole::RegionServer => vec![(
558+
HBASE_METRICS_PORT_NAME.to_string(),
559+
HBASE_REGIONSERVER_METRICS_PORT,
560+
)],
561+
HbaseRole::RestServer => {
562+
vec![(HBASE_METRICS_PORT_NAME.to_string(), HBASE_REST_METRICS_PORT)]
563+
}
564+
}
565+
}
566+
545567
pub fn service_port(&self, role: &HbaseRole) -> u16 {
546568
match role {
547569
HbaseRole::Master => HBASE_MASTER_PORT,
@@ -550,6 +572,14 @@ impl v1alpha1::HbaseCluster {
550572
}
551573
}
552574

575+
pub fn metrics_port(&self, role: &HbaseRole) -> u16 {
576+
match role {
577+
HbaseRole::Master => HBASE_MASTER_METRICS_PORT,
578+
HbaseRole::RegionServer => HBASE_REGIONSERVER_METRICS_PORT,
579+
HbaseRole::RestServer => HBASE_REST_METRICS_PORT,
580+
}
581+
}
582+
553583
/// Name of the port used by the Web UI, which depends on HTTPS usage
554584
pub fn ui_port_name(&self) -> String {
555585
if self.has_https_enabled() {

rust/operator-binary/src/hbase_controller.rs

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use stackable_operator::{
4545
core::{DeserializeGuard, error_boundary},
4646
runtime::controller::Action,
4747
},
48-
kvp::{Label, LabelError, Labels, ObjectLabels},
48+
kvp::{Annotations, Label, LabelError, Labels, ObjectLabels},
4949
logging::controller::ReconcilerError,
5050
memory::{BinaryMultiple, MemoryQuantity},
5151
product_config_utils::{transform_all_roles_to_config, validate_all_roles_and_groups_config},
@@ -427,6 +427,14 @@ pub async fn reconcile_hbase(
427427

428428
let rg_service =
429429
build_rolegroup_service(hbase, &hbase_role, &rolegroup, &resolved_product_image)?;
430+
431+
let rg_metrics_service = build_rolegroup_metrics_service(
432+
hbase,
433+
&hbase_role,
434+
&rolegroup,
435+
&resolved_product_image,
436+
)?;
437+
430438
let rg_configmap = build_rolegroup_config_map(
431439
hbase,
432440
&client.kubernetes_cluster_info,
@@ -452,6 +460,12 @@ pub async fn reconcile_hbase(
452460
.with_context(|_| ApplyRoleGroupServiceSnafu {
453461
rolegroup: rolegroup.clone(),
454462
})?;
463+
cluster_resources
464+
.add(client, rg_metrics_service)
465+
.await
466+
.with_context(|_| ApplyRoleGroupServiceSnafu {
467+
rolegroup: rolegroup.clone(),
468+
})?;
455469
cluster_resources
456470
.add(client, rg_configmap)
457471
.await
@@ -739,12 +753,9 @@ fn build_rolegroup_service(
739753
})
740754
.collect();
741755

742-
let prometheus_label =
743-
Label::try_from(("prometheus.io/scrape", "true")).context(BuildLabelSnafu)?;
744-
745756
let metadata = ObjectMetaBuilder::new()
746757
.name_and_namespace(hbase)
747-
.name(headless_service_name(&rolegroup.object_name()))
758+
.name(rolegroup.rolegroup_headless_service_name())
748759
.ownerreference_from_resource(hbase, None, Some(true))
749760
.context(ObjectMissingMetadataForOwnerRefSnafu)?
750761
.with_recommended_labels(build_recommended_labels(
@@ -754,7 +765,6 @@ fn build_rolegroup_service(
754765
&rolegroup.role_group,
755766
))
756767
.context(ObjectMetaSnafu)?
757-
.with_label(prometheus_label)
758768
.build();
759769

760770
let service_selector =
@@ -778,6 +788,82 @@ fn build_rolegroup_service(
778788
})
779789
}
780790

791+
/// The rolegroup metrics [`Service`] is a service that exposes metrics and a prometheus scraping label.
792+
pub fn build_rolegroup_metrics_service(
793+
hbase: &v1alpha1::HbaseCluster,
794+
hbase_role: &HbaseRole,
795+
rolegroup: &RoleGroupRef<v1alpha1::HbaseCluster>,
796+
resolved_product_image: &ResolvedProductImage,
797+
) -> Result<Service, Error> {
798+
let ports = hbase
799+
.metrics_ports(hbase_role)
800+
.into_iter()
801+
.map(|(name, value)| ServicePort {
802+
name: Some(name),
803+
port: i32::from(value),
804+
protocol: Some("TCP".to_string()),
805+
..ServicePort::default()
806+
})
807+
.collect();
808+
809+
let service_selector =
810+
Labels::role_group_selector(hbase, APP_NAME, &rolegroup.role, &rolegroup.role_group)
811+
.context(BuildLabelSnafu)?;
812+
813+
Ok(Service {
814+
metadata: ObjectMetaBuilder::new()
815+
.name_and_namespace(hbase)
816+
.name(rolegroup.rolegroup_metrics_service_name())
817+
.ownerreference_from_resource(hbase, None, Some(true))
818+
.context(ObjectMissingMetadataForOwnerRefSnafu)?
819+
.with_recommended_labels(build_recommended_labels(
820+
hbase,
821+
&resolved_product_image.app_version_label_value,
822+
&rolegroup.role,
823+
&rolegroup.role_group,
824+
))
825+
.context(ObjectMetaSnafu)?
826+
.with_label(Label::try_from(("prometheus.io/scrape", "true")).context(LabelBuildSnafu)?)
827+
.with_annotations(prometheus_annotations(hbase, hbase_role))
828+
.build(),
829+
spec: Some(ServiceSpec {
830+
// Internal communication does not need to be exposed
831+
type_: Some("ClusterIP".to_string()),
832+
cluster_ip: Some("None".to_string()),
833+
ports: Some(ports),
834+
selector: Some(service_selector.into()),
835+
publish_not_ready_addresses: Some(true),
836+
..ServiceSpec::default()
837+
}),
838+
status: None,
839+
})
840+
}
841+
842+
/// Common annotations for Prometheus
843+
///
844+
/// These annotations can be used in a ServiceMonitor.
845+
///
846+
/// see also <https://github.com/prometheus-community/helm-charts/blob/prometheus-27.32.0/charts/prometheus/values.yaml#L983-L1036>
847+
fn prometheus_annotations(hbase: &v1alpha1::HbaseCluster, hbase_role: &HbaseRole) -> Annotations {
848+
Annotations::try_from([
849+
("prometheus.io/path".to_owned(), "/prometheus".to_owned()),
850+
(
851+
"prometheus.io/port".to_owned(),
852+
hbase.metrics_port(hbase_role).to_string(),
853+
),
854+
(
855+
"prometheus.io/scheme".to_owned(),
856+
if hbase.has_https_enabled() {
857+
"https".to_owned()
858+
} else {
859+
"http".to_owned()
860+
},
861+
),
862+
("prometheus.io/scrape".to_owned(), "true".to_owned()),
863+
])
864+
.expect("should be valid annotations")
865+
}
866+
781867
/// The rolegroup [`StatefulSet`] runs the rolegroup, as configured by the administrator.
782868
///
783869
/// The [`Pod`](`stackable_operator::k8s_openapi::api::core::v1::Pod`)s are accessible through the corresponding [`Service`] (from [`build_rolegroup_service`]).
@@ -1088,7 +1174,7 @@ fn build_rolegroup_statefulset(
10881174
match_labels: Some(statefulset_match_labels.into()),
10891175
..LabelSelector::default()
10901176
},
1091-
service_name: Some(headless_service_name(&rolegroup_ref.object_name())),
1177+
service_name: Some(rolegroup_ref.rolegroup_headless_service_name()),
10921178
template: pod_template,
10931179
volume_claim_templates: listener_pvc,
10941180
..StatefulSetSpec::default()
@@ -1198,10 +1284,6 @@ fn build_hbase_env_sh(
11981284
Ok(result)
11991285
}
12001286

1201-
fn headless_service_name(role_group_name: &str) -> String {
1202-
format!("{name}-headless", name = role_group_name)
1203-
}
1204-
12051287
#[cfg(test)]
12061288
mod test {
12071289
use rstest::rstest;

0 commit comments

Comments
 (0)