//! Service configuration for Prometheus, Loki, Grafana, Promtail, tracer, and a caller-provided binary
use crate::aws::{
s3::{DEPLOYMENTS_PREFIX, TOOLS_BINARIES_PREFIX, TOOLS_CONFIGS_PREFIX, WGET},
Architecture,
};
// Binary artifacts and user SSH state live under this directory. NVMe-backed instances mount
// instance-store storage here so existing binary configs use NVMe without extra configuration.
const HOME_DIRECTORY: &str = "/home/ubuntu";
// Install path for the Docker static binary.
const DOCKER_BIN: &str = "/usr/local/bin/docker";
/// Deployer version used to namespace static configs in S3
const DEPLOYER_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Version of the Grafana Node Exporter Full dashboard to provision.
///
/// The public dashboard is available at:
///
pub const GRAFANA_NODE_EXPORTER_DASHBOARD_VERSION: &str = "45";
/// Version of Docker static binaries to download and install
pub const DOCKER_VERSION: &str = "29.6.0";
/// Version of Samply to download and install
pub const SAMPLY_VERSION: &str = "0.13.1";
/// Version of logrotate package for Ubuntu 24.04
pub const LOGROTATE_VERSION: &str = "3.21.0-2build1";
/// Ubuntu package archive base URL for arm64
const UBUNTU_ARCHIVE_ARM64: &str = "http://ports.ubuntu.com/ubuntu-ports/pool";
/// Ubuntu package archive base URL for x86_64
const UBUNTU_ARCHIVE_X86_64: &str = "http://archive.ubuntu.com/ubuntu/pool";
/// Image for Prometheus metrics storage and querying
pub const PROMETHEUS_IMAGE: &str = "prom/prometheus:v3.2.0";
/// Image for Promtail log forwarding
pub const PROMTAIL_IMAGE: &str = "grafana/promtail:3.4.2";
/// Image for Node Exporter system metrics
pub const NODE_EXPORTER_IMAGE: &str = "prom/node-exporter:v1.9.0";
/// Image for Loki log storage and querying
pub const LOKI_IMAGE: &str = "grafana/loki:3.4.2";
/// Image for Tempo trace storage and querying
pub const TEMPO_IMAGE: &str = "grafana/tempo:2.7.1";
/// Image for Pyroscope profile storage and querying
pub const PYROSCOPE_IMAGE: &str = "grafana/pyroscope:1.12.0";
/// Image for Grafana dashboards
pub const GRAFANA_IMAGE: &str = "grafana/grafana:11.5.2";
/// Image for Tracer trace viewing
pub const TRACER_IMAGE: &str = "ghcr.io/clabby/tracer-web:0.2.1";
#[derive(Clone, Copy)]
struct ImageService {
service: &'static str,
description: &'static str,
image: &'static str,
network_host: bool,
pid_host: bool,
user: Option<&'static str>,
env: &'static [(&'static str, &'static str)],
volumes: &'static [&'static str],
args: &'static [&'static str],
options: &'static [&'static str],
after: &'static [&'static str],
}
const NODE_EXPORTER_ARGS: &[&str] = &[
"--path.procfs=/host/proc",
"--path.sysfs=/host/sys",
"--path.rootfs=/host/rootfs",
"--collector.filesystem.mount-points-exclude=^/(dev|proc|sys|var/lib/docker/.+|var/lib/containers/.+)($|/)",
];
const NODE_EXPORTER_VOLUMES: &[&str] = &[
"/proc:/host/proc:ro",
"/sys:/host/sys:ro",
"/:/host/rootfs:ro,rslave",
];
const PROMTAIL_VOLUMES: &[&str] = &[
"/etc/promtail:/etc/promtail:ro",
"/var/log:/var/log:ro",
"/var/lib/promtail:/var/lib/promtail",
];
const MONITORING_IMAGE_SERVICES: &[ImageService] = &[
ImageService {
service: "node_exporter",
description: "Node Exporter",
image: NODE_EXPORTER_IMAGE,
network_host: true,
pid_host: true,
user: Some("0:0"),
env: &[],
volumes: NODE_EXPORTER_VOLUMES,
args: NODE_EXPORTER_ARGS,
options: &[],
after: &[],
},
ImageService {
service: "prometheus",
description: "Prometheus Monitoring Service",
image: PROMETHEUS_IMAGE,
network_host: true,
pid_host: false,
user: Some("0:0"),
env: &[],
volumes: &[
"/opt/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro",
"/opt/prometheus/data:/prometheus",
],
args: &[
"--config.file=/etc/prometheus/prometheus.yml",
"--storage.tsdb.path=/prometheus",
],
options: &[],
after: &["node_exporter.service"],
},
ImageService {
service: "loki",
description: "Loki Log Aggregation Service",
image: LOKI_IMAGE,
network_host: true,
pid_host: false,
user: Some("0:0"),
env: &[],
volumes: &["/etc/loki/loki.yml:/etc/loki/loki.yml:ro", "/loki:/loki"],
args: &["-config.file=/etc/loki/loki.yml"],
options: &[],
after: &[],
},
ImageService {
service: "pyroscope",
description: "Pyroscope Profiling Service",
image: PYROSCOPE_IMAGE,
network_host: true,
pid_host: false,
user: Some("0:0"),
env: &[],
volumes: &[
"/etc/pyroscope/pyroscope.yml:/etc/pyroscope/pyroscope.yml:ro",
"/var/lib/pyroscope:/var/lib/pyroscope",
],
args: &["--config.file=/etc/pyroscope/pyroscope.yml"],
options: &[],
after: &[],
},
ImageService {
service: "tempo",
description: "Tempo Tracing Service",
image: TEMPO_IMAGE,
network_host: true,
pid_host: false,
user: Some("0:0"),
env: &[],
volumes: &[
"/etc/tempo/tempo.yml:/etc/tempo/tempo.yml:ro",
"/tempo:/var/tempo",
],
args: &["-config.file=/etc/tempo/tempo.yml"],
options: &[],
after: &[],
},
ImageService {
service: "grafana",
description: "Grafana Dashboard Service",
image: GRAFANA_IMAGE,
network_host: true,
pid_host: false,
user: Some("0:0"),
env: &[
("GF_AUTH_ANONYMOUS_ENABLED", "true"),
("GF_AUTH_ANONYMOUS_ORG_ROLE", "Admin"),
("GF_INSTALL_PLUGINS", "grafana-pyroscope-app"),
],
volumes: &[
"/etc/grafana/provisioning:/etc/grafana/provisioning:ro",
"/var/lib/grafana:/var/lib/grafana",
],
args: &[],
options: &[],
after: &[
"prometheus.service",
"loki.service",
"tempo.service",
"pyroscope.service",
],
},
ImageService {
service: "tracer",
description: "Tracer Trace Viewer",
image: TRACER_IMAGE,
network_host: true,
pid_host: false,
user: None,
env: &[("TEMPO_URL", "http://127.0.0.1:3200")],
volumes: &[],
args: &[],
options: &[],
after: &["tempo.service"],
},
];
const BINARY_IMAGE_SERVICES: &[ImageService] = &[
ImageService {
service: "promtail",
description: "Promtail Log Forwarder",
image: PROMTAIL_IMAGE,
network_host: true,
pid_host: false,
user: Some("0:0"),
env: &[],
volumes: PROMTAIL_VOLUMES,
args: &["-config.file=/etc/promtail/promtail.yml"],
options: &[],
after: &["binary.service"],
},
ImageService {
service: "node_exporter",
description: "Node Exporter",
image: NODE_EXPORTER_IMAGE,
network_host: true,
pid_host: true,
user: Some("0:0"),
env: &[],
volumes: NODE_EXPORTER_VOLUMES,
args: NODE_EXPORTER_ARGS,
options: &[],
after: &[],
},
];
impl ImageService {
fn service_file(self) -> String {
let after = self.after.join(" ");
let mut run_options = String::new();
if self.network_host {
run_options.push_str(" --network host");
}
if self.pid_host {
run_options.push_str(" --pid host");
}
if let Some(user) = self.user {
run_options.push_str(" --user ");
run_options.push_str(user);
}
for (key, value) in self.env {
run_options.push_str(" --env ");
run_options.push_str(key);
run_options.push('=');
run_options.push_str(value);
}
for volume in self.volumes {
run_options.push_str(" --volume ");
run_options.push_str(volume);
}
for option in self.options {
run_options.push(' ');
run_options.push_str(option);
}
let args = if self.args.is_empty() {
String::new()
} else {
format!(" {}", self.args.join(" "))
};
let after_line = if after.is_empty() {
"After=network-online.target docker.service".to_string()
} else {
format!("After=network-online.target docker.service {after}")
};
format!(
r#"[Unit]
Description={description}
Wants=network-online.target
{after_line}
Requires=docker.service
[Service]
ExecStartPre=-{docker_bin} rm -f {service}
ExecStart={docker_bin} run --rm --name {service}{run_options} {image}{args}
TimeoutStopSec=60
Restart=always
[Install]
WantedBy=multi-user.target
"#,
service = self.service,
description = self.description,
docker_bin = DOCKER_BIN,
image = self.image,
)
}
}
/// Maps an upstream image reference to a filesystem- and S3-safe token (e.g. `prom_prometheus_v3.2.0`).
pub(crate) fn sanitize_image(image: &str) -> String {
image.replace(['/', ':'], "_")
}
/// Returns the on-instance filename for an image tarball (e.g. `prom_prometheus_v3.2.0.tar.gz`).
pub(crate) fn image_file_name(image: &str) -> String {
format!("{}.tar.gz", sanitize_image(image))
}
/// Returns the S3 key for a cached image tarball (`docker save` output, per architecture).
pub fn image_s3_key(image: &str, architecture: Architecture) -> String {
format!(
"{TOOLS_BINARIES_PREFIX}/images/{name}/linux-{arch}/image.tar.gz",
name = sanitize_image(image),
arch = architecture.as_str(),
)
}
/// Returns the distinct images required by the monitoring instance.
pub(crate) fn monitoring_images() -> impl Iterator- {
distinct_images(MONITORING_IMAGE_SERVICES)
}
/// Returns the distinct images required by binary instances.
pub(crate) fn binary_images() -> impl Iterator
- {
distinct_images(BINARY_IMAGE_SERVICES)
}
fn distinct_images(services: &'static [ImageService]) -> impl Iterator
- {
let mut seen = Vec::new();
services.iter().filter_map(move |service| {
if seen.contains(&service.image) {
None
} else {
seen.push(service.image);
Some(service.image)
}
})
}
pub(crate) fn docker_bin_s3_key(version: &str, architecture: Architecture) -> String {
let arch = docker_static_arch(architecture);
format!("{TOOLS_BINARIES_PREFIX}/docker/{version}/linux-{arch}/docker-{version}.tgz")
}
pub(crate) fn samply_bin_s3_key(version: &str, architecture: Architecture) -> String {
let arch = match architecture {
Architecture::Arm64 => "aarch64",
Architecture::X86_64 => "x86_64",
};
format!("{TOOLS_BINARIES_PREFIX}/samply/{version}/linux-{arch}/samply-{arch}-unknown-linux-gnu.tar.xz")
}
pub(crate) fn logrotate_bin_s3_key(version: &str, architecture: Architecture) -> String {
format!(
"{TOOLS_BINARIES_PREFIX}/logrotate/{version}/linux-{arch}/logrotate_{version}_{arch}.deb",
arch = architecture.as_str()
)
}
// S3 key functions for component configs and services (include deployer version for cache invalidation)
//
// Convention: {TOOLS_CONFIGS_PREFIX}/{deployer_version}/{component}/{file}
pub fn grafana_datasources_s3_key() -> String {
format!("{TOOLS_CONFIGS_PREFIX}/{DEPLOYER_VERSION}/grafana/datasources.yml")
}
pub fn grafana_dashboards_s3_key() -> String {
format!("{TOOLS_CONFIGS_PREFIX}/{DEPLOYER_VERSION}/grafana/all.yml")
}
pub fn grafana_node_exporter_dashboard_s3_key(version: &str) -> String {
format!(
"{TOOLS_CONFIGS_PREFIX}/{DEPLOYER_VERSION}/grafana/node-exporter-full-revision-{version}.json"
)
}
pub fn loki_config_s3_key() -> String {
format!("{TOOLS_CONFIGS_PREFIX}/{DEPLOYER_VERSION}/loki/config.yml")
}
pub fn pyroscope_config_s3_key() -> String {
format!("{TOOLS_CONFIGS_PREFIX}/{DEPLOYER_VERSION}/pyroscope/config.yml")
}
pub fn tempo_config_s3_key() -> String {
format!("{TOOLS_CONFIGS_PREFIX}/{DEPLOYER_VERSION}/tempo/config.yml")
}
// S3 key functions for pyroscope agent (lives with pyroscope component)
pub fn pyroscope_agent_service_s3_key() -> String {
format!("{TOOLS_CONFIGS_PREFIX}/{DEPLOYER_VERSION}/pyroscope/agent.service")
}
pub fn pyroscope_agent_timer_s3_key() -> String {
format!("{TOOLS_CONFIGS_PREFIX}/{DEPLOYER_VERSION}/pyroscope/agent.timer")
}
// S3 key functions for binary instance configs
pub(crate) fn binary_service_s3_key_for_arch(architecture: Architecture) -> String {
format!(
"{TOOLS_CONFIGS_PREFIX}/{DEPLOYER_VERSION}/binary/service-{arch}",
arch = architecture.as_str()
)
}
/// Returns the S3 key for an instance's binary by digest (deduplicated within deployment)
pub fn binary_s3_key(tag: &str, digest: &str) -> String {
format!("{DEPLOYMENTS_PREFIX}/{tag}/binaries/{digest}")
}
/// Returns the S3 key for an instance's config by digest (deduplicated within deployment)
pub fn config_s3_key(tag: &str, digest: &str) -> String {
format!("{DEPLOYMENTS_PREFIX}/{tag}/configs/{digest}")
}
/// Returns the S3 key for hosts.yaml by digest (deduplicated within deployment)
pub fn hosts_s3_key(tag: &str, digest: &str) -> String {
format!("{DEPLOYMENTS_PREFIX}/{tag}/hosts/{digest}")
}
/// Returns the S3 key for promtail config by digest (deduplicated within deployment)
pub fn promtail_s3_key(tag: &str, digest: &str) -> String {
format!("{DEPLOYMENTS_PREFIX}/{tag}/promtail/{digest}")
}
/// Returns the S3 key for pyroscope agent script by digest (deduplicated within deployment)
pub fn pyroscope_s3_key(tag: &str, digest: &str) -> String {
format!("{DEPLOYMENTS_PREFIX}/{tag}/pyroscope/{digest}")
}
/// Returns the S3 key for monitoring config by digest (deduplicated within deployment)
pub fn monitoring_s3_key(tag: &str, digest: &str) -> String {
format!("{DEPLOYMENTS_PREFIX}/{tag}/monitoring/{digest}")
}
/// Returns the download URL for the Node Exporter Full Grafana dashboard
pub(crate) fn grafana_node_exporter_dashboard_download_url(version: &str) -> String {
format!("https://grafana.com/api/dashboards/1860/revisions/{version}/download")
}
/// Returns the download URL for Samply from GitHub
pub(crate) fn samply_download_url(version: &str, architecture: Architecture) -> String {
let arch = match architecture {
Architecture::Arm64 => "aarch64",
Architecture::X86_64 => "x86_64",
};
format!(
"https://github.com/mstange/samply/releases/download/samply-v{version}/samply-{arch}-unknown-linux-gnu.tar.xz"
)
}
/// Returns the download URL for Docker static binaries
pub(crate) fn docker_download_url(version: &str, architecture: Architecture) -> String {
format!(
"https://download.docker.com/linux/static/stable/{arch}/docker-{version}.tgz",
arch = docker_static_arch(architecture)
)
}
const fn docker_static_arch(architecture: Architecture) -> &'static str {
match architecture {
Architecture::Arm64 => "aarch64",
Architecture::X86_64 => "x86_64",
}
}
/// Returns the download URL for logrotate from Ubuntu archive
pub(crate) fn logrotate_download_url(version: &str, architecture: Architecture) -> String {
let base = match architecture {
Architecture::Arm64 => UBUNTU_ARCHIVE_ARM64,
Architecture::X86_64 => UBUNTU_ARCHIVE_X86_64,
};
format!(
"{base}/main/l/logrotate/logrotate_{version}_{arch}.deb",
arch = architecture.as_str()
)
}
/// YAML configuration for Grafana datasources (Prometheus, Loki, Tempo, and Pyroscope)
pub const DATASOURCES_YML: &str = r#"
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://localhost:9090
access: proxy
isDefault: true
- name: Loki
type: loki
url: http://localhost:3100
access: proxy
isDefault: false
- name: Tempo
type: tempo
url: http://localhost:3200
access: proxy
isDefault: false
- name: Pyroscope
type: grafana-pyroscope-datasource
url: http://localhost:4040
access: proxy
isDefault: false
"#;
/// YAML configuration for Grafana dashboard providers
pub const ALL_YML: &str = r#"
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
options:
path: /var/lib/grafana/dashboards
"#;
/// YAML configuration for Loki
pub const LOKI_CONFIG: &str = r#"
auth_enabled: false
target: all
server:
http_listen_port: 3100
grpc_listen_port: 9095
common:
ring:
kvstore:
store: inmemory
replication_factor: 1
instance_addr: 127.0.0.1
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /loki/index
cache_location: /loki/index_cache
filesystem:
directory: /loki/chunks
table_manager:
retention_deletes_enabled: true
retention_period: 12h
compactor:
working_directory: /loki/compactor
ingester:
wal:
dir: /loki/wal
"#;
/// YAML configuration for Pyroscope
pub const PYROSCOPE_CONFIG: &str = r#"
target: all
server:
http_listen_port: 4040
grpc_listen_port: 0
pyroscopedb:
data_path: /var/lib/pyroscope
self_profiling:
disable_push: true
"#;
/// YAML configuration for Tempo
pub const TEMPO_CONFIG: &str = r#"
server:
grpc_listen_port: 9096
grpc_server_max_recv_msg_size: 104857600
grpc_server_max_send_msg_size: 104857600
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
http:
endpoint: "0.0.0.0:4318"
storage:
trace:
backend: local
local:
path: /var/tempo/traces
wal:
path: /var/tempo/wal
ingester:
max_block_duration: 1h
compactor:
compaction:
block_retention: 1h
compaction_cycle: 1h
overrides:
defaults:
ingestion:
rate_limit_bytes: 1000000000
burst_size_bytes: 2000000000
max_traces_per_user: 500000
global:
max_bytes_per_trace: 100000000
"#;
/// URLs for monitoring service installation
pub struct MonitoringUrls {
pub docker_tgz: String,
pub prometheus_config: String,
pub datasources_yml: String,
pub all_yml: String,
pub dashboard: String,
pub node_exporter_dashboard: String,
pub loki_yml: String,
pub pyroscope_yml: String,
pub tempo_yml: String,
pub images: Vec<(&'static str, String)>,
}
/// Builds the concurrent download + verify block for the per-architecture image tarballs.
fn image_download_block(images: &[(&'static str, String)]) -> String {
if images.is_empty() {
return String::new();
}
let mut cmd = String::from(
"\n# Download container image tarballs from S3 concurrently via pre-signed URLs\nmkdir -p /home/ubuntu/images\nrm -f /home/ubuntu/images/*.tar.gz\n",
);
let mut files = Vec::new();
for (image, url) in images {
let file = image_file_name(image);
cmd.push_str(WGET);
cmd.push_str(" -O /home/ubuntu/images/");
cmd.push_str(&file);
cmd.push_str(" '");
cmd.push_str(url);
cmd.push_str("' &\n");
files.push(file);
}
cmd.push_str("wait\n");
cmd.push_str("for f in");
for file in &files {
cmd.push(' ');
cmd.push_str(file);
}
cmd.push_str(
"; do\n if [ ! -s \"/home/ubuntu/images/$f\" ]; then\n echo \"ERROR: Failed to download image $f\" >&2\n exit 1\n fi\ndone\n",
);
cmd
}
/// Phase 1: Download files from S3 on monitoring instance
pub(crate) fn install_monitoring_download_cmd(urls: &MonitoringUrls) -> String {
let mut cmd = format!(
r#"
# Clean up any previous download artifacts (allows retries to re-download fresh copies)
rm -f /home/ubuntu/prometheus.yml /home/ubuntu/datasources.yml /home/ubuntu/all.yml \
/home/ubuntu/dashboard.json /home/ubuntu/node-exporter-full.json /home/ubuntu/loki.yml \
/home/ubuntu/pyroscope.yml /home/ubuntu/tempo.yml /home/ubuntu/docker.tgz
# Unmask services in case previous attempt left them masked
sudo systemctl unmask docker prometheus loki pyroscope tempo node_exporter grafana tracer 2>/dev/null || true
# Download all files from S3 concurrently via pre-signed URLs
{WGET} -O /home/ubuntu/docker.tgz '{}' &
{WGET} -O /home/ubuntu/prometheus.yml '{}' &
{WGET} -O /home/ubuntu/datasources.yml '{}' &
{WGET} -O /home/ubuntu/all.yml '{}' &
{WGET} -O /home/ubuntu/dashboard.json '{}' &
{WGET} -O /home/ubuntu/node-exporter-full.json '{}' &
{WGET} -O /home/ubuntu/loki.yml '{}' &
{WGET} -O /home/ubuntu/pyroscope.yml '{}' &
{WGET} -O /home/ubuntu/tempo.yml '{}' &
wait
# Verify all downloads succeeded
for f in prometheus.yml datasources.yml all.yml dashboard.json node-exporter-full.json \
loki.yml pyroscope.yml tempo.yml docker.tgz; do
if [ ! -f "/home/ubuntu/$f" ]; then
echo "ERROR: Failed to download $f" >&2
exit 1
fi
done
"#,
urls.docker_tgz,
urls.prometheus_config,
urls.datasources_yml,
urls.all_yml,
urls.dashboard,
urls.node_exporter_dashboard,
urls.loki_yml,
urls.pyroscope_yml,
urls.tempo_yml,
);
cmd.push_str(&image_download_block(&urls.images));
cmd
}
fn install_image_services_cmd(services: &'static [ImageService]) -> String {
if services.is_empty() {
return String::new();
}
let mut cmd = String::from(
r#"# Install Docker services
rm -rf /home/ubuntu/docker
tar xzf /home/ubuntu/docker.tgz -C /home/ubuntu
sudo install -m 0755 /home/ubuntu/docker/* /usr/local/bin/
sudo tee /etc/systemd/system/docker.service >/dev/null <<'EOF'
[Unit]
Description=Docker Application Container Engine
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/local/bin/dockerd --host=unix:///var/run/docker.sock
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutStartSec=120
Restart=always
RestartSec=2
Delegate=yes
KillMode=process
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
OOMScoreAdjust=-500
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now docker
"#,
);
// Load the container images from the tarballs downloaded from S3 (`docker load` auto-detects
// the gzip compression), then delete each tarball so the loaded Docker layers and the archive
// don't both occupy the (small) root volume.
for image in distinct_images(services) {
let file = image_file_name(image);
cmd.push_str("sudo docker load -i /home/ubuntu/images/");
cmd.push_str(&file);
cmd.push('\n');
cmd.push_str("sudo rm -f /home/ubuntu/images/");
cmd.push_str(&file);
cmd.push('\n');
}
for service in services {
cmd.push_str("sudo tee /etc/systemd/system/");
cmd.push_str(service.service);
cmd.push_str(".service >/dev/null <<'EOF'\n");
cmd.push_str(&service.service_file());
cmd.push_str("EOF\n");
}
cmd
}
pub(crate) fn monitoring_image_services() -> impl Iterator
- {
MONITORING_IMAGE_SERVICES
.iter()
.map(|service| service.service)
}
pub(crate) fn binary_image_services() -> impl Iterator
- {
BINARY_IMAGE_SERVICES.iter().map(|service| service.service)
}
/// Phase 2: Setup services on monitoring instance (does not start them)
pub(crate) fn install_monitoring_setup_cmd() -> String {
let image_services = install_image_services_cmd(MONITORING_IMAGE_SERVICES);
format!(
r#"set -e
# Enable BBR congestion control
echo -e "net.core.default_qdisc=fq\nnet.ipv4.tcp_congestion_control=bbr" | sudo tee /etc/sysctl.d/99-bbr.conf >/dev/null && sudo sysctl -p /etc/sysctl.d/99-bbr.conf
{image_services}
# Create service directories
sudo mkdir -p /opt/prometheus /opt/prometheus/data
sudo mkdir -p /loki/index /loki/index_cache /loki/chunks /loki/compactor /loki/wal
sudo mkdir -p /var/lib/pyroscope
sudo mkdir -p /tempo/traces /tempo/wal
sudo mkdir -p /etc/grafana/provisioning/datasources /etc/grafana/provisioning/dashboards /var/lib/grafana/dashboards
# Install configuration files
sudo mv /home/ubuntu/prometheus.yml /opt/prometheus/prometheus.yml
sudo mv /home/ubuntu/datasources.yml /etc/grafana/provisioning/datasources/datasources.yml
sudo mv /home/ubuntu/all.yml /etc/grafana/provisioning/dashboards/all.yml
sudo mv /home/ubuntu/dashboard.json /var/lib/grafana/dashboards/dashboard.json
sudo mv /home/ubuntu/node-exporter-full.json /var/lib/grafana/dashboards/node-exporter-full.json
sudo mkdir -p /etc/loki
sudo mv /home/ubuntu/loki.yml /etc/loki/loki.yml
sudo chown root:root /etc/loki/loki.yml
sudo mkdir -p /etc/pyroscope
sudo mv /home/ubuntu/pyroscope.yml /etc/pyroscope/pyroscope.yml
sudo chown root:root /etc/pyroscope/pyroscope.yml
sudo mkdir -p /etc/tempo
sudo mv /home/ubuntu/tempo.yml /etc/tempo/tempo.yml
sudo chown root:root /etc/tempo/tempo.yml
"#,
)
}
/// Continuation of monitoring install command (services startup)
pub(crate) fn start_monitoring_services_cmd() -> String {
let mut cmd = String::from(
r#"set -e
# Start services
sudo systemctl daemon-reload
"#,
);
for service in monitoring_image_services() {
cmd.push_str("sudo systemctl start ");
cmd.push_str(service);
cmd.push('\n');
cmd.push_str("sudo systemctl enable ");
cmd.push_str(service);
cmd.push('\n');
}
cmd
}
/// URLs for binary instance installation
pub struct InstanceUrls {
pub binary: String,
pub config: String,
pub hosts: String,
pub promtail_config: String,
pub binary_service: String,
pub pyroscope_script: String,
pub pyroscope_service: String,
pub pyroscope_timer: String,
pub docker_tgz: String,
pub logrotate_deb: String,
pub images: Vec<(&'static str, String)>,
}
/// Phase 1 (optional): Install apt packages on binary instances
/// Only needed when profiling is enabled or NVMe instance-store devices are mounted.
pub(crate) fn install_binary_apt_cmd(profiling: bool, nvme: bool) -> Option {
let mut packages = Vec::new();
if profiling {
packages.extend([
"linux-tools-common",
"linux-tools-generic",
"linux-tools-$(uname -r)",
]);
}
if nvme {
packages.push("mdadm");
}
if packages.is_empty() {
return None;
}
Some(format!(
r#"set -e
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y {}
"#,
packages.join(" ")
))
}
/// Phase 2: Download files from S3 on binary instances
pub(crate) fn install_binary_download_cmd(urls: &InstanceUrls) -> String {
let mut cmd = format!(
r#"
# Clean up any previous download artifacts (allows retries to re-download fresh copies)
rm -f /home/ubuntu/binary /home/ubuntu/config.conf /home/ubuntu/hosts.yaml \
/home/ubuntu/promtail.yml /home/ubuntu/binary.service \
/home/ubuntu/pyroscope-agent.sh /home/ubuntu/pyroscope-agent.service \
/home/ubuntu/pyroscope-agent.timer /home/ubuntu/docker.tgz \
/home/ubuntu/logrotate.deb
# Unmask services in case previous attempt left them masked
sudo systemctl unmask docker promtail node_exporter binary 2>/dev/null || true
# Download all files from S3 concurrently via pre-signed URLs
{WGET} -O /home/ubuntu/binary '{}' &
{WGET} -O /home/ubuntu/config.conf '{}' &
{WGET} -O /home/ubuntu/hosts.yaml '{}' &
{WGET} -O /home/ubuntu/promtail.yml '{}' &
{WGET} -O /home/ubuntu/binary.service '{}' &
{WGET} -O /home/ubuntu/pyroscope-agent.sh '{}' &
{WGET} -O /home/ubuntu/pyroscope-agent.service '{}' &
{WGET} -O /home/ubuntu/pyroscope-agent.timer '{}' &
{WGET} -O /home/ubuntu/docker.tgz '{}' &
{WGET} -O /home/ubuntu/logrotate.deb '{}' &
wait
# Verify all downloads succeeded
for f in binary config.conf hosts.yaml promtail.yml binary.service \
pyroscope-agent.sh pyroscope-agent.service pyroscope-agent.timer \
docker.tgz logrotate.deb; do
if [ ! -f "/home/ubuntu/$f" ]; then
echo "ERROR: Failed to download $f" >&2
exit 1
fi
done
"#,
urls.binary,
urls.config,
urls.hosts,
urls.promtail_config,
urls.binary_service,
urls.pyroscope_script,
urls.pyroscope_service,
urls.pyroscope_timer,
urls.docker_tgz,
urls.logrotate_deb,
);
cmd.push_str(&image_download_block(&urls.images));
cmd
}
/// Returns a command that mounts EC2 NVMe instance-store storage at the binary working directory.
pub(crate) fn nvme_setup_cmd() -> String {
format!(
r#"
set -e
cd /
# Configure EC2 NVMe instance-store mounting
NVME_MOUNT='{mount_directory}'
sudo udevadm settle || true
NVME_DEVICES="$(for model_path in /sys/block/nvme*n1/device/model; do
[ -e "$model_path" ] || continue
if grep -q 'Amazon EC2 NVMe Instance Storage' "$model_path"; then
basename "$(dirname "$(dirname "$model_path")")" | sed 's#^#/dev/#'
fi
done | sort)"
NVME_COUNT="$(printf '%s\n' "$NVME_DEVICES" | sed '/^$/d' | wc -l)"
if [ "$NVME_COUNT" -eq 0 ]; then
echo "ERROR: NVMe instance storage requested but no EC2 NVMe instance-store devices were found" >&2
exit 1
fi
sudo mkdir -p "$NVME_MOUNT"
if [ "$NVME_COUNT" -eq 1 ]; then
NVME_TARGET="$(printf '%s\n' "$NVME_DEVICES" | head -n1)"
else
NVME_TARGET=/dev/md/commonware-nvme
sudo mkdir -p /dev/md
sudo mdadm --assemble "$NVME_TARGET" $NVME_DEVICES >/dev/null 2>&1 || true
if [ ! -e "$NVME_TARGET" ]; then
sudo mdadm --create "$NVME_TARGET" --name=commonware-nvme --level=0 --raid-devices="$NVME_COUNT" --force $NVME_DEVICES
fi
sudo udevadm settle || true
fi
if ! sudo blkid "$NVME_TARGET" >/dev/null 2>&1; then
sudo mkfs.ext4 -F "$NVME_TARGET"
fi
if ! findmnt -rn --mountpoint "$NVME_MOUNT" >/dev/null; then
NVME_STAGE="$(mktemp -d /tmp/commonware-nvme.XXXXXX)"
cleanup_stage() {{
if [ -n "${{NVME_STAGE:-}}" ]; then
sudo umount "$NVME_STAGE" >/dev/null 2>&1 || true
sudo rmdir "$NVME_STAGE" >/dev/null 2>&1 || true
fi
}}
trap cleanup_stage EXIT
sudo mount "$NVME_TARGET" "$NVME_STAGE"
sudo tar -C "$NVME_MOUNT" -cpf - . | sudo tar -C "$NVME_STAGE" -xpf -
sudo umount "$NVME_STAGE"
sudo rmdir "$NVME_STAGE"
NVME_STAGE=
sudo mount "$NVME_TARGET" "$NVME_MOUNT"
fi
NVME_MOUNT_SOURCE="$(findmnt -rn --mountpoint "$NVME_MOUNT" -o SOURCE || true)"
if [ -z "$NVME_MOUNT_SOURCE" ]; then
echo "ERROR: NVMe instance storage was not mounted at $NVME_MOUNT" >&2
exit 1
fi
if [ "$(readlink -f "$NVME_MOUNT_SOURCE")" != "$(readlink -f "$NVME_TARGET")" ]; then
echo "ERROR: $NVME_MOUNT is mounted from $NVME_MOUNT_SOURCE, expected $NVME_TARGET" >&2
exit 1
fi
sudo chown -R ubuntu:ubuntu "$NVME_MOUNT"
"#,
mount_directory = HOME_DIRECTORY,
)
}
/// Phase 3: Setup and start services on binary instances
pub(crate) fn install_binary_setup_cmd(profiling: bool) -> String {
let image_services = install_image_services_cmd(BINARY_IMAGE_SERVICES);
let perf_setup = if profiling {
r#"
# Setup pyroscope agent (perf symlink must be created after linux-tools installed via apt)
sudo ln -sf "$(find /usr/lib/linux-tools/*/perf | head -1)" /usr/local/bin/perf
sudo chmod +x /home/ubuntu/pyroscope-agent.sh
sudo mv /home/ubuntu/pyroscope-agent.service /etc/systemd/system/pyroscope-agent.service
sudo mv /home/ubuntu/pyroscope-agent.timer /etc/systemd/system/pyroscope-agent.timer
"#
} else {
""
};
let pyroscope_enable = if profiling {
"\nsudo systemctl enable --now pyroscope-agent.timer\n"
} else {
""
};
format!(
r#"set -e
# Enable BBR congestion control and tune TCP buffers for cross-region flows
echo -e "net.core.default_qdisc=fq\nnet.ipv4.tcp_congestion_control=bbr\nnet.core.rmem_max=16777216\nnet.core.wmem_max=16777216\nnet.ipv4.tcp_rmem=4096 2097152 16777216\nnet.ipv4.tcp_wmem=4096 262144 16777216\nnet.ipv4.tcp_slow_start_after_idle=0" | sudo tee /etc/sysctl.d/99-bbr.conf >/dev/null && sudo sysctl -p /etc/sysctl.d/99-bbr.conf
# The default_qdisc sysctl only applies to interfaces attached after it is
# set, so install fq (which enforces BBR's pacing) on every tx queue of the
# primary interface explicitly and persist the setup across reboots.
sudo tee /usr/local/bin/setup-qdisc.sh >/dev/null <<'EOF'
#!/bin/bash
set -e
IFACE=$(ip -o route get 8.8.8.8 | awk '{{for (i = 1; i < NF; i++) if ($i == "dev") print $(i + 1)}}')
# Single-queue interfaces reject mq, so pace on the root instead.
if tc qdisc replace dev "$IFACE" root handle 1: mq; then
NQ=$(ls -d "/sys/class/net/$IFACE/queues/tx-"* | wc -l)
for h in $(seq 1 "$NQ"); do
tc qdisc del dev "$IFACE" parent "1:$(printf '%x' "$h")" 2>/dev/null || true
tc qdisc add dev "$IFACE" parent "1:$(printf '%x' "$h")" fq
done
else
tc qdisc replace dev "$IFACE" root fq
fi
EOF
sudo chmod +x /usr/local/bin/setup-qdisc.sh
sudo tee /etc/systemd/system/setup-qdisc.service >/dev/null <<'EOF'
[Unit]
Description=Install fq pacing qdisc on the primary interface
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/setup-qdisc.sh
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now setup-qdisc.service
{image_services}
# Install deb packages
sudo dpkg -i /home/ubuntu/logrotate.deb
# Setup Promtail
sudo mkdir -p /etc/promtail /var/lib/promtail
sudo mv /home/ubuntu/promtail.yml /etc/promtail/promtail.yml
sudo chown root:root /etc/promtail/promtail.yml
# Setup binary
chmod +x /home/ubuntu/binary
sudo touch /var/log/binary.log && sudo chown ubuntu:ubuntu /var/log/binary.log
sudo mv /home/ubuntu/binary.service /etc/systemd/system/binary.service
# Setup logrotate
sudo tee /etc/logrotate.d/binary >/dev/null <<'EOF'
{LOGROTATE_CONF}EOF
echo "0 * * * * /usr/sbin/logrotate /etc/logrotate.d/binary" | crontab -
{perf_setup}
# Start services
sudo systemctl daemon-reload
sudo systemctl enable node_exporter
sudo systemctl enable promtail
sudo systemctl enable binary
sudo systemctl start node_exporter
sudo systemctl start promtail
# Don't abort the setup script if the caller-provided binary fails to start; the deployer
# polls the service afterwards and warns on an unhealthy binary (see poll_service_active)
# so create can still complete and the binary can be fixed with a later update.
sudo systemctl start binary || true{pyroscope_enable}"#
)
}
/// Generates Promtail configuration with the monitoring instance's private IP and instance name
pub fn promtail_config(
monitoring_private_ip: &str,
instance_name: &str,
ip: &str,
region: &str,
arch: &str,
) -> String {
format!(
r#"
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /var/lib/promtail/positions.yaml
clients:
- url: http://{monitoring_private_ip}:3100/loki/api/v1/push
scrape_configs:
- job_name: binary_logs
static_configs:
- targets:
- localhost
labels:
deployer_name: {instance_name}
deployer_ip: {ip}
deployer_region: {region}
deployer_arch: {arch}
__path__: /var/log/binary.log
"#
)
}
/// Generates Prometheus configuration with scrape targets for all instance IPs
pub fn generate_prometheus_config(instances: &[(&str, &str, &str, &str)]) -> String {
let mut config = String::from(
r#"
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'monitoring_system'
static_configs:
- targets: ['localhost:9100']
"#,
);
for (name, ip, region, arch) in instances {
config.push_str(&format!(
r#"
- job_name: '{name}_binary'
static_configs:
- targets: ['{ip}:9090']
labels:
deployer_name: '{name}'
deployer_ip: '{ip}'
deployer_region: '{region}'
deployer_arch: '{arch}'
"#
));
config.push_str(&format!(
r#"
- job_name: '{name}_system'
static_configs:
- targets: ['{ip}:9100']
labels:
deployer_name: '{name}'
deployer_ip: '{ip}'
deployer_region: '{region}'
deployer_arch: '{arch}'
"#
));
}
config
}
/// Logrotate configuration for binary logs
pub const LOGROTATE_CONF: &str = r#"
/var/log/binary.log {
rotate 0
copytruncate
missingok
notifempty
}
"#;
/// Generates systemd service file content for the deployed binary
pub(crate) fn binary_service() -> String {
String::from(
r#"[Unit]
Description=Deployed Binary Service
After=network.target
[Service]
ExecStart=/home/ubuntu/binary --hosts=/home/ubuntu/hosts.yaml --config=/home/ubuntu/config.conf
TimeoutStopSec=60
Restart=always
User=ubuntu
LimitNOFILE=infinity
StandardOutput=append:/var/log/binary.log
StandardError=append:/var/log/binary.log
[Install]
WantedBy=multi-user.target
"#,
)
}
/// Shell script content for the Pyroscope agent (perf + wget)
pub fn generate_pyroscope_script(
monitoring_private_ip: &str,
name: &str,
ip: &str,
region: &str,
arch: &str,
) -> String {
format!(
r#"#!/bin/bash
set -e
SERVICE_NAME="binary.service"
PERF_DATA_FILE="/tmp/perf.data"
PERF_STACK_FILE="/tmp/perf.stack"
PROFILE_DURATION=60 # seconds
PERF_FREQ=100 # Hz
# Construct the Pyroscope application name with tags (URL-encoded)
RAW_APP_NAME="binary{{deployer_name={name},deployer_ip={ip},deployer_region={region},deployer_arch={arch}}}"
APP_NAME=$(printf '%s' "$RAW_APP_NAME" | python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.stdin.read()))")
# Get the PID of the binary service
PID=$(systemctl show --property MainPID ${{SERVICE_NAME}} | cut -d= -f2)
if [ -z "$PID" ] || [ "$PID" -eq 0 ]; then
echo "Error: Could not get PID for ${{SERVICE_NAME}}." >&2
exit 1
fi
# Record performance data
echo "Recording perf data for PID ${{PID}}..."
sudo perf record -F ${{PERF_FREQ}} -p ${{PID}} -o ${{PERF_DATA_FILE}} -g --call-graph fp -- sleep ${{PROFILE_DURATION}}
# Generate folded stack report
echo "Generating folded stack report..."
sudo perf report -i ${{PERF_DATA_FILE}} --stdio --no-children -g folded,0,caller,count -s comm | \
awk '/^[0-9]+\.[0-9]+%/ {{ comm = $2 }} /^[0-9]/ {{ print comm ";" substr($0, index($0, $2)), $1 }}' > ${{PERF_STACK_FILE}}
# Check if stack file is empty (perf might fail silently sometimes)
if [ ! -s "${{PERF_STACK_FILE}}" ]; then
echo "Warning: ${{PERF_STACK_FILE}} is empty. Skipping upload." >&2
# Clean up empty perf.data
sudo rm -f ${{PERF_DATA_FILE}} ${{PERF_STACK_FILE}}
exit 0
fi
# Calculate timestamps
UNTIL_TS=$(date +%s)
FROM_TS=$((UNTIL_TS - PROFILE_DURATION))
# Upload to Pyroscope
echo "Uploading profile to Pyroscope at {monitoring_private_ip}..."
wget --post-file="${{PERF_STACK_FILE}}" \
--header="Content-Type: text/plain" \
--quiet \
-O /dev/null \
"http://{monitoring_private_ip}:4040/ingest?name=${{APP_NAME}}&format=folded&units=samples&aggregationType=sum&sampleType=cpu&from=${{FROM_TS}}&until=${{UNTIL_TS}}&spyName=perf"
echo "Profile upload complete."
sudo rm -f ${{PERF_DATA_FILE}} ${{PERF_STACK_FILE}}
"#
)
}
/// Systemd service file content for the Pyroscope agent script
pub const PYROSCOPE_AGENT_SERVICE: &str = r#"[Unit]
Description=Pyroscope Agent (Perf Script Runner)
Wants=network-online.target
After=network-online.target binary.service
[Service]
Type=oneshot
User=ubuntu
ExecStart=/home/ubuntu/pyroscope-agent.sh
[Install]
WantedBy=multi-user.target
"#;
/// Systemd timer file content for the Pyroscope agent service
pub const PYROSCOPE_AGENT_TIMER: &str = r#"[Unit]
Description=Run Pyroscope Agent periodically
[Timer]
# Wait a bit after boot before the first run
OnBootSec=2min
# Run roughly every minute after the last run finished
OnUnitInactiveSec=1min
Unit=pyroscope-agent.service
# Randomize the delay to avoid thundering herd
RandomizedDelaySec=10s
[Install]
WantedBy=timers.target
"#;
#[cfg(test)]
mod tests {
use super::*;
fn monitoring_urls() -> MonitoringUrls {
MonitoringUrls {
docker_tgz: "docker".to_string(),
prometheus_config: "prometheus-config".to_string(),
datasources_yml: "datasources".to_string(),
all_yml: "dashboards".to_string(),
dashboard: "dashboard".to_string(),
node_exporter_dashboard: "node-exporter-dashboard".to_string(),
loki_yml: "loki-config".to_string(),
pyroscope_yml: "pyroscope-config".to_string(),
tempo_yml: "tempo-config".to_string(),
images: monitoring_images()
.map(|image| (image, format!("image-url-{image}")))
.collect(),
}
}
fn instance_urls() -> InstanceUrls {
InstanceUrls {
binary: "binary".to_string(),
config: "config".to_string(),
hosts: "hosts".to_string(),
promtail_config: "promtail-config".to_string(),
binary_service: "binary-service".to_string(),
pyroscope_script: "pyroscope-script".to_string(),
pyroscope_service: "pyroscope-service".to_string(),
pyroscope_timer: "pyroscope-timer".to_string(),
docker_tgz: "docker".to_string(),
logrotate_deb: "logrotate".to_string(),
images: binary_images()
.map(|image| (image, format!("image-url-{image}")))
.collect(),
}
}
#[test]
fn test_binary_s3_keys_arm64() {
let arch = Architecture::Arm64;
assert_eq!(
docker_bin_s3_key("29.6.0", arch),
"tools/binaries/docker/29.6.0/linux-aarch64/docker-29.6.0.tgz"
);
assert_eq!(
samply_bin_s3_key("0.13.1", arch),
"tools/binaries/samply/0.13.1/linux-aarch64/samply-aarch64-unknown-linux-gnu.tar.xz"
);
assert_eq!(
logrotate_bin_s3_key("3.21.0-2build1", arch),
"tools/binaries/logrotate/3.21.0-2build1/linux-arm64/logrotate_3.21.0-2build1_arm64.deb"
);
}
#[test]
fn test_binary_s3_keys_x86_64() {
let arch = Architecture::X86_64;
assert_eq!(
docker_bin_s3_key("29.6.0", arch),
"tools/binaries/docker/29.6.0/linux-x86_64/docker-29.6.0.tgz"
);
assert_eq!(
samply_bin_s3_key("0.13.1", arch),
"tools/binaries/samply/0.13.1/linux-x86_64/samply-x86_64-unknown-linux-gnu.tar.xz"
);
assert_eq!(
logrotate_bin_s3_key("3.21.0-2build1", arch),
"tools/binaries/logrotate/3.21.0-2build1/linux-amd64/logrotate_3.21.0-2build1_amd64.deb"
);
}
#[test]
fn test_config_s3_keys() {
let version = DEPLOYER_VERSION;
assert_eq!(
grafana_datasources_s3_key(),
format!("tools/configs/{version}/grafana/datasources.yml")
);
assert_eq!(
grafana_dashboards_s3_key(),
format!("tools/configs/{version}/grafana/all.yml")
);
assert_eq!(
grafana_node_exporter_dashboard_s3_key(GRAFANA_NODE_EXPORTER_DASHBOARD_VERSION),
format!("tools/configs/{version}/grafana/node-exporter-full-revision-{GRAFANA_NODE_EXPORTER_DASHBOARD_VERSION}.json")
);
assert_eq!(
loki_config_s3_key(),
format!("tools/configs/{version}/loki/config.yml")
);
assert_eq!(
pyroscope_config_s3_key(),
format!("tools/configs/{version}/pyroscope/config.yml")
);
assert_eq!(
tempo_config_s3_key(),
format!("tools/configs/{version}/tempo/config.yml")
);
assert_eq!(
pyroscope_agent_service_s3_key(),
format!("tools/configs/{version}/pyroscope/agent.service")
);
assert_eq!(
pyroscope_agent_timer_s3_key(),
format!("tools/configs/{version}/pyroscope/agent.timer")
);
assert_eq!(
binary_service_s3_key_for_arch(Architecture::Arm64),
format!("tools/configs/{version}/binary/service-arm64")
);
assert_eq!(
binary_service_s3_key_for_arch(Architecture::X86_64),
format!("tools/configs/{version}/binary/service-amd64")
);
}
#[test]
fn test_monitoring_installs_node_exporter_dashboard() {
let urls = monitoring_urls();
let download = install_monitoring_download_cmd(&urls);
assert!(download.contains("-O /home/ubuntu/docker.tgz"));
assert!(download.contains("-O /home/ubuntu/node-exporter-full.json"));
assert!(download.contains("node-exporter-dashboard"));
assert!(download.contains("node-exporter-full.json"));
let setup = install_monitoring_setup_cmd();
assert!(setup.contains(
"sudo mv /home/ubuntu/dashboard.json /var/lib/grafana/dashboards/dashboard.json"
));
assert!(setup.contains("sudo mv /home/ubuntu/node-exporter-full.json /var/lib/grafana/dashboards/node-exporter-full.json"));
}
#[test]
fn test_monitoring_installs_image_services() {
let urls = monitoring_urls();
let download = install_monitoring_download_cmd(&urls);
assert!(!download.contains("tracer.service"));
assert!(!download.contains("prometheus.tar.gz"));
assert!(!download.contains("grafana.deb"));
assert!(!download.contains("node_exporter.tar.gz"));
assert!(download.contains(&format!(
"-O /home/ubuntu/images/{}",
image_file_name(TRACER_IMAGE)
)));
assert!(download.contains(&format!(
"-O /home/ubuntu/images/{}",
image_file_name(PROMETHEUS_IMAGE)
)));
let setup = install_monitoring_setup_cmd();
assert!(setup.contains("# Install Docker services"));
assert!(setup.contains("tar xzf /home/ubuntu/docker.tgz -C /home/ubuntu"));
assert!(
setup.contains("ExecStart=/usr/local/bin/dockerd --host=unix:///var/run/docker.sock")
);
assert!(setup.contains("ExecStartPre=-/usr/local/bin/docker rm -f tracer"));
assert!(setup.contains("ExecStart=/usr/local/bin/docker run --rm --name tracer"));
assert!(!setup.contains("/usr/bin/docker"));
assert!(!setup.contains("apt-get install -y docker.io"));
assert!(!setup.contains("docker pull"));
assert!(!setup.contains("docker login"));
for image in [
PROMETHEUS_IMAGE,
LOKI_IMAGE,
PYROSCOPE_IMAGE,
TEMPO_IMAGE,
GRAFANA_IMAGE,
NODE_EXPORTER_IMAGE,
TRACER_IMAGE,
] {
assert!(setup.contains(&format!(
"sudo docker load -i /home/ubuntu/images/{}",
image_file_name(image)
)));
assert!(setup.contains(&format!(
"sudo rm -f /home/ubuntu/images/{}",
image_file_name(image)
)));
}
assert!(setup.contains("sudo tee /etc/systemd/system/tracer.service"));
assert!(setup.contains("sudo tee /etc/systemd/system/grafana.service"));
let start = start_monitoring_services_cmd();
for service in monitoring_image_services() {
assert!(start.contains(&format!("sudo systemctl start {service}")));
assert!(start.contains(&format!("sudo systemctl enable {service}")));
}
assert!(setup.contains(&format!(
"--env TEMPO_URL=http://127.0.0.1:3200 {TRACER_IMAGE}"
)));
assert!(setup.contains("--network host"));
assert!(setup.contains("--volume /tempo:/var/tempo"));
assert!(!setup.contains("--volume /tempo:/tempo"));
assert!(TEMPO_CONFIG.contains("path: /var/tempo/traces"));
assert!(TEMPO_CONFIG.contains("path: /var/tempo/wal"));
assert!(TEMPO_CONFIG.contains("grpc_server_max_recv_msg_size: 104857600"));
assert!(TEMPO_CONFIG.contains("grpc_server_max_send_msg_size: 104857600"));
assert!(!TEMPO_CONFIG.contains("path: /tempo/"));
}
#[test]
fn test_prometheus_config_scrapes_binary_metrics_for_all_instances() {
let config = generate_prometheus_config(&[
("validator", "10.0.0.1", "us-east-1", "arm64"),
("spammer", "10.0.0.2", "us-east-1", "arm64"),
]);
assert!(config.contains("job_name: 'validator_binary'"));
assert!(config.contains("targets: ['10.0.0.1:9090']"));
assert!(config.contains("job_name: 'validator_system'"));
assert!(config.contains("targets: ['10.0.0.1:9100']"));
assert!(config.contains("job_name: 'spammer_binary'"));
assert!(config.contains("targets: ['10.0.0.2:9090']"));
assert!(config.contains("job_name: 'spammer_system'"));
assert!(config.contains("targets: ['10.0.0.2:9100']"));
}
#[test]
fn test_binary_installs_image_helpers() {
let urls = instance_urls();
let download = install_binary_download_cmd(&urls);
assert!(download.contains("-O /home/ubuntu/promtail.yml"));
assert!(download.contains("-O /home/ubuntu/docker.tgz"));
assert!(download.contains("-O /home/ubuntu/logrotate.deb"));
assert!(download.contains(&format!(
"-O /home/ubuntu/images/{}",
image_file_name(PROMTAIL_IMAGE)
)));
assert!(download.contains(&format!(
"-O /home/ubuntu/images/{}",
image_file_name(NODE_EXPORTER_IMAGE)
)));
let setup = install_binary_setup_cmd(false);
assert!(setup.contains(&format!(
"sudo docker load -i /home/ubuntu/images/{}",
image_file_name(PROMTAIL_IMAGE)
)));
assert!(setup.contains(&format!(
"sudo docker load -i /home/ubuntu/images/{}",
image_file_name(NODE_EXPORTER_IMAGE)
)));
assert!(!setup.contains("docker pull"));
assert!(!setup.contains("docker login"));
assert!(setup.contains("sudo tee /etc/systemd/system/promtail.service"));
assert!(setup.contains("sudo tee /etc/systemd/system/node_exporter.service"));
assert!(setup.contains("ExecStartPre=-/usr/local/bin/docker rm -f promtail"));
assert!(setup.contains("ExecStart=/usr/local/bin/docker run --rm --name promtail"));
assert!(!setup.contains("/usr/bin/docker"));
assert!(setup.contains("sudo dpkg -i /home/ubuntu/logrotate.deb"));
assert!(setup.contains("copytruncate"));
assert!(setup.contains(
"echo \"0 * * * * /usr/sbin/logrotate /etc/logrotate.d/binary\" | crontab -"
));
assert!(!setup.contains("binary-log-truncate"));
assert!(!setup.contains("busybox"));
assert!(setup.contains("sudo systemctl enable node_exporter"));
assert!(setup.contains("sudo systemctl enable promtail"));
assert!(setup.contains("sudo systemctl enable binary"));
assert!(setup.contains("tc qdisc replace dev \"$IFACE\" root handle 1: mq"));
assert!(setup.contains("tc qdisc replace dev \"$IFACE\" root fq"));
assert!(setup.contains("sudo systemctl enable --now setup-qdisc.service"));
assert!(setup.contains("net.ipv4.tcp_rmem=4096 2097152 16777216"));
assert!(setup.contains("sudo systemctl start node_exporter"));
assert!(setup.contains("sudo systemctl start promtail"));
assert!(setup.contains("sudo systemctl start binary || true"));
let node_exporter_start = setup.find("sudo systemctl start node_exporter").unwrap();
let promtail_start = setup.find("sudo systemctl start promtail").unwrap();
let binary_start = setup.find("sudo systemctl start binary || true").unwrap();
assert!(node_exporter_start < binary_start);
assert!(promtail_start < binary_start);
let promtail = promtail_config("10.0.0.1", "worker", "10.0.1.2", "us-east-1", "arm64");
assert!(promtail.contains("filename: /var/lib/promtail/positions.yaml"));
}
#[test]
fn test_deployment_s3_keys() {
let digest = "abc123def456";
assert_eq!(
binary_s3_key("my-tag", digest),
"deployments/my-tag/binaries/abc123def456"
);
assert_eq!(
config_s3_key("my-tag", digest),
"deployments/my-tag/configs/abc123def456"
);
assert_eq!(
hosts_s3_key("my-tag", digest),
"deployments/my-tag/hosts/abc123def456"
);
assert_eq!(
promtail_s3_key("my-tag", digest),
"deployments/my-tag/promtail/abc123def456"
);
assert_eq!(
pyroscope_s3_key("my-tag", digest),
"deployments/my-tag/pyroscope/abc123def456"
);
assert_eq!(
monitoring_s3_key("my-tag", digest),
"deployments/my-tag/monitoring/abc123def456"
);
}
}