Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ruby app metrics support #172

Merged
merged 40 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
439d266
Introduce new metrics buildpack
schneems Jul 13, 2023
5f449e6
Update build output
schneems Jul 19, 2023
32a611b
Attempting workaround with exec.d
schneems Jul 18, 2023
7074806
Run agentmon with Ubuntu's start-stop-daemon
schneems Jul 19, 2023
669ef6f
Fix spelling
schneems Jul 19, 2023
f247694
Move background loop logic from bash to Rust
schneems Jul 19, 2023
2bba03c
Add metrics integration test with WIP libcnb-test feature
schneems Aug 9, 2023
54605d4
Enable agentmon logging
schneems Aug 9, 2023
cc1684e
Clippy
schneems Aug 9, 2023
0d6cbbc
Fix tool key
schneems Aug 10, 2023
a50891a
Prefix image with `buildpack-`
schneems Aug 10, 2023
8528b0b
Update agentmon_loop
schneems Aug 10, 2023
7bae992
Remove unneeded enum contortions
schneems Aug 10, 2023
c5510f3
Inline binary logic and consolidate test logic
schneems Aug 11, 2023
802b9a6
Hardcode agentmon url, improve caching
schneems Aug 11, 2023
61adc80
Move metrics logic inside of heroku/ruby
schneems Aug 28, 2023
e988a9e
Revert "Update build output"
schneems Aug 28, 2023
be5d071
Changelog entry and fix tests
schneems Aug 28, 2023
ab2e6da
Update buildpacks/ruby/Cargo.toml
schneems Sep 11, 2023
ce540e4
Prefer unwrap_or_else over match with Ok(_)
schneems Sep 11, 2023
2897d75
Remove changelog prefix
schneems Sep 11, 2023
e41ae1f
Prefer unwrap_or_else over match with Ok(_)
schneems Sep 11, 2023
aade4c0
Prefer HasMap::from() over mut HashMap::new()
schneems Sep 11, 2023
dcea764
Fix docs
schneems Sep 11, 2023
a7d3d4c
Use `Path::try_exists()` instead of `exists()`
schneems Sep 11, 2023
b68c177
Fix stringly typed errors
schneems Sep 11, 2023
283e600
Env var key is static & add disable instructions
schneems Sep 11, 2023
991adb4
Static env var keys and tests
schneems Sep 11, 2023
abcf16d
Make error more specific
schneems Sep 11, 2023
daa1074
Simplify spawning daemon
schneems Sep 11, 2023
b5540ad
Match functionality to function name
schneems Sep 11, 2023
8e40054
Log when barnes is installed, or isn't
schneems Sep 11, 2023
81d2fa4
Address process booting determinism
schneems Sep 12, 2023
98ab37c
Update buildpacks/ruby/src/layers/metrics_agent_install.rs
schneems Sep 12, 2023
6627345
Remove unneeded BuildpackReference::Crate
schneems Sep 12, 2023
40a5b81
Fix accidentally removed line
schneems Sep 12, 2023
41c12fe
Remove unused workspace declarations
schneems Sep 12, 2023
4ef43ee
Add download checksum for agentmon TGZ
schneems Sep 18, 2023
43cbaa6
Update changelog to use Keep a Changelog format
schneems Sep 18, 2023
f4abf4d
Prefer shared tooling
schneems Sep 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 148 additions & 55 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
[workspace]
resolver = "2"
members = [
"buildpacks/ruby",
"commons"
]
members = ["buildpacks/ruby", "commons"]
9 changes: 7 additions & 2 deletions buildpacks/ruby/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Introduce heroku build metrics support (https://github.com/heroku/buildpacks-ruby/pull/172)
- Changelog moved to be per-crate rather than for the whole project (https://github.com/heroku/buildpacks-ruby/pull/154)

## [2.0.1] - 2023-07-25

- Commons: Introduce `build_output` module (https://github.com/heroku/buildpacks-ruby/pull/155)
Expand All @@ -19,4 +24,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Version 2.0.0 for the first release is not a typo. There was an issue in pack where a builder with the same name and version number would reuse artifacts left on image from [prior runs which caused issues](https://github.com/buildpacks/pack/issues/1322#issuecomment-1038241038). There were prior releases of `heroku/ruby` CNB from different sources that triggered this problem. To ensure no one would encounter that issue we developed and released using a version we know has not been used before. Version 2.0 was the first major version without a prior release of `heroku/ruby` CNB from any source.

[unreleased]: https://github.com/heroku/buildpacks-ruby/compare/v2.0.1...HEAD
[2.0.1]: https://github.com/heroku/buildpacks-ruby/releases/tag/v2.0.1
[2.0.1]: https://github.com/heroku/buildpacks-ruby/releases/tag/v2.0.1
2 changes: 2 additions & 0 deletions buildpacks/ruby/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ tempfile = "3"
thiserror = "1"
ureq = "2"
url = "2"
clap = { version = "4", features = ["derive"] }
sha2 = "0.10.7"

[dev-dependencies]
libcnb-test = "=0.14.0"
Expand Down
207 changes: 207 additions & 0 deletions buildpacks/ruby/src/bin/agentmon_loop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Enable Clippy lints that are disabled by default.
// https://rust-lang.github.io/rust-clippy/stable/index.html
#![warn(clippy::pedantic)]

use clap::Parser;
use std::ffi::OsStr;
use std::process::ExitStatus;
use std::{
collections::HashMap,
path::{Path, PathBuf},
process::{exit, Command},
thread::sleep,
time::Duration,
};

static PORT: &str = "PORT";
static DYNO: &str = "DYNO";
static AGENTMON_DEBUG: &str = "AGENTMON_DEBUG";
static HEROKU_METRICS_URL: &str = "HEROKU_METRICS_URL";

const SLEEP_FOR: Duration = Duration::from_secs(1);

/// Agentmon Loop
///
/// Boots agentmon (a statsd server) in a loop
///
/// Example:
///
/// ```shell
/// $ cargo run --bin agentmon_loop -- --path <path/to/agentmon/binary>
/// ```

/// Turn CLI arguments into a Rust struct
#[derive(Parser, Debug)]
struct Args {
/// Path to the agentmon executable e.g. --path <path/to/agentmon/binary>
#[arg(short, long)]
path: PathBuf,
}

fn main() {
let agentmon = Args::parse().path;
let agentmon_args = build_args(&std::env::vars().collect::<HashMap<String, String>>())
.unwrap_or_else(|error| {
eprintln!("Cannot start agentmon. {error}");
exit(1)
});

match agentmon.try_exists() {
Ok(true) => {
eprintln!("Booting agentmon_loop");
loop {
match run(&agentmon, &agentmon_args) {
Ok(status) => {
eprintln!("Process completed with status={status}, sleeping {SLEEP_FOR:?}");
}
Err(error) => {
eprintln!(
"Process could not run due to error. {error}, sleeping {SLEEP_FOR:?}"
);
}
};
sleep(SLEEP_FOR);
}
}
Ok(false) => {
eprintln!("Path does not exist {path}", path = agentmon.display());
exit(1);
}
Err(error) => {
eprintln!(
"Could not access {path}. {error}",
path = agentmon.display()
);
exit(1);
}
}
}

/// Print and run executable
///
/// Runs an executable at the given path with args and streams the results.
fn run<I, S>(path: &Path, args: I) -> Result<ExitStatus, std::io::Error>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut cmd = Command::new(path);
cmd.args(args);

eprintln!("Running: {}", commons::fun_run::display(&mut cmd));

cmd.status()
}

#[derive(Debug, thiserror::Error, PartialEq)]
enum BuildArgsError {
#[error("{PORT} environment variable is not set")]
MissingPort,

#[error("{HEROKU_METRICS_URL} environment variable is not set")]
MissingMetricsUrl,

#[error("One off dyno detected i.e. {DYNO}=\"run.*\"")]
RunDynoDetected,
}

/// Constructs the arguments for agentmon based on environment variables
///
/// # Errors
///
/// - Environment variables: PORT or `HEROKU_METRICS_URL` are not set
/// - Environment variable DYNO starts with `run.`
fn build_args(env: &HashMap<String, String>) -> Result<Vec<String>, BuildArgsError> {
let mut args = Vec::new();
if env.get(DYNO).is_some_and(|value| value.starts_with("run.")) {
return Err(BuildArgsError::RunDynoDetected);
}

if let Some(port) = env.get(PORT) {
args.push(format!("-statsd-addr=:{port}"));
} else {
return Err(BuildArgsError::MissingPort);
};

if env.get(AGENTMON_DEBUG).is_some_and(|value| value == "true") {
args.push("-debug".to_string());
};

if let Some(url) = env.get(HEROKU_METRICS_URL) {
args.push(url.clone());
} else {
return Err(BuildArgsError::MissingMetricsUrl);
};

Ok(args)
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn missing_run_dyno() {
let result = build_args(&HashMap::from([("DYNO".to_string(), "run.1".to_string())]));

assert_eq!(result, Err(BuildArgsError::RunDynoDetected));
}

#[test]
fn missing_metrics_url() {
let result = build_args(&HashMap::from([("PORT".to_string(), "123".to_string())]));

assert_eq!(result, Err(BuildArgsError::MissingMetricsUrl));
}

#[test]
fn missing_port() {
let result = build_args(&HashMap::new());

assert_eq!(result, Err(BuildArgsError::MissingPort));
}

#[test]
fn agentmon_statsd_addr() {
let env = HashMap::from([
("PORT".to_string(), "90210".to_string()),
(
"HEROKU_METRICS_URL".to_string(),
"https://example.com".to_string(),
),
]);

let result = build_args(&env);

assert_eq!(
result,
Ok(vec![
"-statsd-addr=:90210".to_string(),
"https://example.com".to_string()
])
);
}

#[test]
fn agentmon_debug_args() {
let env = HashMap::from([
("PORT".to_string(), "90210".to_string()),
(
"HEROKU_METRICS_URL".to_string(),
"https://example.com".to_string(),
),
("AGENTMON_DEBUG".to_string(), "true".to_string()),
]);

let result = build_args(&env);

assert_eq!(
result,
Ok(vec![
"-statsd-addr=:90210".to_string(),
"-debug".to_string(),
"https://example.com".to_string()
])
);
}
}
112 changes: 112 additions & 0 deletions buildpacks/ruby/src/bin/launch_daemon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use clap::Parser;
use std::path::PathBuf;
use std::process::exit;
use std::process::Command;

static AGENTMON_DEBUG: &str = "AGENTMON_DEBUG";

/// Schedules agentmon to run as a background daemon

/// CLI argument parser
///
/// ```shell
/// $ cargo run --bin launch_daemon \
/// --log <path/to/log.txt> \
/// --agentmon <path/to/agentmon> \
/// --loop-path <path/to/agentmon_loop>
/// ```
#[derive(Parser, Debug)]
struct Args {
#[arg(long, value_parser = absolute_path_exists)]
log: PathBuf,

#[arg(long, value_parser = absolute_path_exists)]
agentmon: PathBuf,

#[arg(long, value_parser = absolute_path_exists)]
loop_path: PathBuf,
}

#[derive(Debug, thiserror::Error)]
enum ParseAbsoluteError {
#[error("Cannot determine cannonical path for {0}. {1}")]
CannotCanonicalize(PathBuf, std::io::Error),

#[error("Path does not exist {0}")]
DoesNotExist(PathBuf),

#[error("Cannot read {0}. {1}")]
CannotRead(PathBuf, std::io::Error),
}

/// Used to validate a path pased to the CLI exists and is accessible
fn absolute_path_exists(input: &str) -> Result<PathBuf, ParseAbsoluteError> {
let input = PathBuf::from(input);
let path = input
.canonicalize()
.map_err(|error| ParseAbsoluteError::CannotCanonicalize(input, error))?;

if path
.try_exists()
.map_err(|error| ParseAbsoluteError::CannotRead(path.clone(), error))?
{
Ok(path)
} else {
Err(ParseAbsoluteError::DoesNotExist(path))
}
}

fn main() {
let Args {
log,
loop_path,
agentmon,
} = Args::parse();

let mut command = Command::new("start-stop-daemon");
if let Some(value) = std::env::var_os(AGENTMON_DEBUG) {
fs_err::write(
&log,
format!(
"Logging enabled via `{AGENTMON_DEBUG}={value:?}`. To disable `unset {AGENTMON_DEBUG}`"
),
)
.unwrap_or_else(|error| {
eprintln!(
"Could not write to log file {}. Reason: {error}",
log.display()
)
});

command.args(["--output", &log.to_string_lossy()]);
} else {
fs_err::write(
&log,
format!("To enable logging run with {AGENTMON_DEBUG}=1"),
)
.unwrap_or_else(|error| {
eprintln!(
"Could not write to log file {}. Reason: {error}",
log.display()
)
});
}

command.args([
"--start",
"--background",
"--exec",
&loop_path.to_string_lossy(),
"--",
"--path",
&agentmon.to_string_lossy(),
]);

command.status().unwrap_or_else(|error| {
eprintln!(
"Command failed {}. Details: {error}",
commons::fun_run::display(&mut command)
);
exit(1)
});
}
1 change: 1 addition & 0 deletions buildpacks/ruby/src/layers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod bundle_download_layer;
mod bundle_install_layer;
pub(crate) mod metrics_agent_install;
mod ruby_install_layer;

pub(crate) use self::bundle_download_layer::BundleDownloadLayer;
Expand Down
Loading