Skip to content

Commit

Permalink
Add diagnostics reporting (#264)
Browse files Browse the repository at this point in the history
* Add diagnostics reporting

* Some tidying

* Remove injected failure

* Update URL

* Fixups

* Fix tests

* Use triples instead of architecture
  • Loading branch information
Hoverbear authored Feb 24, 2023
1 parent 8de35b2 commit 19dd7a1
Show file tree
Hide file tree
Showing 14 changed files with 460 additions and 21 deletions.
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ build-inputs = ["darwin.apple_sdk.frameworks.Security"]
build-inputs = ["darwin.apple_sdk.frameworks.Security"]

[features]
default = ["cli"]
default = ["cli", "diagnostics"]
cli = ["eyre", "color-eyre", "clap", "tracing-subscriber", "tracing-error", "atty"]
diagnostics = ["os-release"]

[[bin]]
name = "nix-installer"
Expand Down Expand Up @@ -53,6 +54,8 @@ rand = { version = "0.8.5", default-features = false, features = [ "std", "std_r
semver = { version = "1.0.14", default-features = false, features = ["serde", "std"] }
term = { version = "0.7.0", default-features = false }
uuid = { version = "1.2.2", features = ["serde"] }
os-release = { version = "0.1.0", default-features = false, optional = true }
strum = { version = "0.24.1", features = ["derive"] }

[dev-dependencies]
eyre = { version = "0.6.8", default-features = false, features = [ "track-caller" ] }
Expand Down
11 changes: 10 additions & 1 deletion src/action/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ impl Planner for MyPlanner {
Ok(map)
}
#[cfg(feature = "diagnostics")]
async fn diagnostic_data(&self) -> Result<nix_installer::diagnostics::DiagnosticData, PlannerError> {
Ok(nix_installer::diagnostics::DiagnosticData::new(
self.common.diagnostic_endpoint.clone(),
self.typetag_name().into(),
self.configured_settings().await?,
))
}
}
# async fn custom_planner_install() -> color_eyre::Result<()> {
Expand Down Expand Up @@ -244,7 +253,7 @@ impl ActionDescription {
}

/// An error occurring during an action
#[derive(thiserror::Error, Debug)]
#[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
pub enum ActionError {
/// A custom error
#[error(transparent)]
Expand Down
7 changes: 4 additions & 3 deletions src/cli/subcommand/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub struct Install {
global = true
)]
pub explain: bool,

#[clap(env = "NIX_INSTALLER_PLAN")]
pub plan: Option<PathBuf>,

Expand Down Expand Up @@ -133,12 +134,12 @@ impl CommandExecute for Install {
let res = builtin_planner.plan().await;
match res {
Ok(plan) => plan,
Err(e) => {
if let Some(expected) = e.expected() {
Err(err) => {
if let Some(expected) = err.expected() {
eprintln!("{}", expected.red());
return Ok(ExitCode::FAILURE);
}
return Err(e.into())
return Err(err.into())
}
}
},
Expand Down
2 changes: 2 additions & 0 deletions src/cli/subcommand/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub struct Uninstall {
global = true
)]
pub no_confirm: bool,

#[clap(
long,
env = "NIX_INSTALLER_EXPLAIN",
Expand All @@ -36,6 +37,7 @@ pub struct Uninstall {
global = true
)]
pub explain: bool,

#[clap(default_value = RECEIPT_LOCATION)]
pub receipt: PathBuf,
}
Expand Down
156 changes: 156 additions & 0 deletions src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*! Diagnostic reporting functionality
When enabled with the `diagnostics` feature (default) this module provides automated install success/failure reporting to an endpoint.
That endpoint can be a URL such as `https://our.project.org/nix-installer/diagnostics` or `file:///home/$USER/diagnostic.json` which receives a [`DiagnosticReport`] in JSON format.
*/

use std::time::Duration;

use os_release::OsRelease;
use reqwest::Url;

/// The static of an action attempt
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub enum DiagnosticStatus {
Cancelled,
Success,
/// This includes the [`strum::IntoStaticStr`] representation of the error, we take special care not to include parameters of the error (which may include secrets)
Failure(String),
Pending,
}

/// The action attempted
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Copy)]
pub enum DiagnosticAction {
Install,
Uninstall,
}

/// A report sent to an endpoint
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct DiagnosticReport {
pub version: String,
pub planner: String,
pub configured_settings: Vec<String>,
pub os_name: String,
pub os_version: String,
pub triple: String,
pub action: DiagnosticAction,
pub status: DiagnosticStatus,
}

/// A preparation of data to be sent to the `endpoint`.
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Default)]
pub struct DiagnosticData {
version: String,
planner: String,
configured_settings: Vec<String>,
os_name: String,
os_version: String,
triple: String,
endpoint: Option<Url>,
}

impl DiagnosticData {
pub fn new(endpoint: Option<Url>, planner: String, configured_settings: Vec<String>) -> Self {
let (os_name, os_version) = match OsRelease::new() {
Ok(os_release) => (os_release.name, os_release.version),
Err(_) => ("unknown".into(), "unknown".into()),
};
Self {
endpoint,
version: env!("CARGO_PKG_VERSION").into(),
planner,
configured_settings,
os_name,
os_version,
triple: target_lexicon::HOST.to_string(),
}
}

pub fn report(&self, action: DiagnosticAction, status: DiagnosticStatus) -> DiagnosticReport {
let Self {
version,
planner,
configured_settings,
os_name,
os_version,
triple,
endpoint: _,
} = self;
DiagnosticReport {
version: version.clone(),
planner: planner.clone(),
configured_settings: configured_settings.clone(),
os_name: os_name.clone(),
os_version: os_version.clone(),
triple: triple.clone(),
action,
status,
}
}

#[tracing::instrument(level = "debug", skip_all)]
pub async fn send(
self,
action: DiagnosticAction,
status: DiagnosticStatus,
) -> Result<(), DiagnosticError> {
let serialized = serde_json::to_string_pretty(&self.report(action, status))?;

let endpoint = match self.endpoint {
Some(endpoint) => endpoint,
None => return Ok(()),
};

match endpoint.scheme() {
"https" | "http" => {
tracing::debug!("Sending diagnostic to `{endpoint}`");
let client = reqwest::Client::new();
let res = client
.post(endpoint.clone())
.body(serialized)
.header("Content-Type", "application/json")
.timeout(Duration::from_millis(3000))
.send()
.await;

if let Err(_err) = res {
tracing::info!("Failed to send diagnostic to `{endpoint}`, continuing")
}
},
"file" => {
let path = endpoint.path();
tracing::debug!("Writing diagnostic to `{path}`");
let res = tokio::fs::write(path, serialized).await;

if let Err(_err) = res {
tracing::info!("Failed to send diagnostic to `{path}`, continuing")
}
},
_ => return Err(DiagnosticError::UnknownUrlScheme),
};
Ok(())
}
}

#[derive(thiserror::Error, Debug)]
pub enum DiagnosticError {
#[error("Unknown url scheme")]
UnknownUrlScheme,
#[error("Request error")]
Reqwest(
#[from]
#[source]
reqwest::Error,
),
#[error("Write path `{0}`")]
Write(std::path::PathBuf, #[source] std::io::Error),
#[error("Serializing receipt")]
Serializing(
#[from]
#[source]
serde_json::Error,
),
}
30 changes: 29 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::PathBuf;
use crate::{action::ActionError, planner::PlannerError, settings::InstallSettingsError};

/// An error occurring during a call defined in this crate
#[derive(thiserror::Error, Debug)]
#[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
pub enum NixInstallerError {
/// An error originating from an [`Action`](crate::action::Action)
#[error("Error executing action")]
Expand Down Expand Up @@ -53,6 +53,15 @@ pub enum NixInstallerError {
#[source]
InstallSettingsError,
),

#[cfg(feature = "diagnostics")]
/// Diagnostic error
#[error("Diagnostic error")]
Diagnostic(
#[from]
#[source]
crate::diagnostics::DiagnosticError,
),
}

pub(crate) trait HasExpectedErrors: std::error::Error + Sized + Send + Sync {
Expand All @@ -70,6 +79,25 @@ impl HasExpectedErrors for NixInstallerError {
NixInstallerError::SemVer(_) => None,
NixInstallerError::Planner(planner_error) => planner_error.expected(),
NixInstallerError::InstallSettings(_) => None,
#[cfg(feature = "diagnostics")]
NixInstallerError::Diagnostic(_) => None,
}
}
}

// #[cfg(feature = "diagnostics")]
// impl NixInstallerError {
// pub fn diagnostic_synopsis(&self) -> &'static str {
// match self {
// NixInstallerError::Action(inner) => inner.into(),
// NixInstallerError::Planner(inner) => inner.into(),
// NixInstallerError::RecordingReceipt(_, _)
// | NixInstallerError::CopyingSelf(_)
// | NixInstallerError::SerializingReceipt(_)
// | NixInstallerError::Cancelled
// | NixInstallerError::SemVer(_)
// | NixInstallerError::Diagnostic(_)
// | NixInstallerError::InstallSettings(_) => self.into(),
// }
// }
// }
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ pub mod action;
mod channel_value;
#[cfg(feature = "cli")]
pub mod cli;
#[cfg(feature = "diagnostics")]
pub mod diagnostics;
mod error;
mod os;
mod plan;
Expand Down
Loading

0 comments on commit 19dd7a1

Please sign in to comment.