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 diagnostics reporting #264

Merged
merged 7 commits into from
Feb 24, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
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
2 changes: 1 addition & 1 deletion src/action/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,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 recieves 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),
cole-h marked this conversation as resolved.
Show resolved Hide resolved
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 architecture: 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,
architecture: 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,
architecture: std::env::consts::ARCH.to_string(),
}
}

pub fn report(&self, action: DiagnosticAction, status: DiagnosticStatus) -> DiagnosticReport {
let Self {
version,
planner,
configured_settings,
os_name,
os_version,
architecture,
endpoint: _,
} = self;
DiagnosticReport {
version: version.clone(),
planner: planner.clone(),
configured_settings: configured_settings.clone(),
os_name: os_name.clone(),
os_version: os_version.clone(),
architecture: architecture.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