Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
55 changes: 54 additions & 1 deletion Cargo.lock

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

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "iroh-services"
version = "0.14.0"
version = "0.15.0"
edition = "2024"
readme = "README.md"
description = "p2p quic connections dialed by public key"
Expand Down Expand Up @@ -33,6 +33,7 @@ tracing-subscriber = { version = "0.3.20", features = [
"fmt",
"json",
] }
tracing-appender = "0.2"
serde_json = "1.0.140"
bytes = { version = "1.10.1", features = ["serde"] }
futures-buffered = "0.2.12"
Expand All @@ -52,6 +53,7 @@ built = { version = "0.8", features = ["cargo-lock"] }
[dev-dependencies]
rand = { version = "0.10", features = ["chacha"] }
temp_env_vars = "0.2.1"
tempfile = "3"
tokio = { version = "1.45", features = ["macros", "rt", "rt-multi-thread", "signal"] }


Expand All @@ -60,3 +62,6 @@ default = []

[[example]]
name = "net_diagnostics"

[[example]]
name = "logs"
93 changes: 93 additions & 0 deletions examples/logs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Log collection example.
//!
//! Demonstrates installing the iroh-services file logger and letting the
//! cloud override the local filter at runtime via `SetLogLevel`.
//!
//! The level filter starts at `off`. The cloud pushes a level after this
//! endpoint is opted into log collection from the dashboard or REST API.
//! Anything emitted before the cloud responds is silently dropped.
//!
//! Run with: cargo run --example logs

use std::time::Duration;

use anyhow::Result;
use iroh::{Endpoint, endpoint::presets, protocol::Router};
use iroh_services::{
API_SECRET_ENV_VAR_NAME, ApiSecret, CLIENT_HOST_ALPN, Client, ClientHost,
caps::LogsCap,
logs::{self, FileLoggerConfig, Rotation},
};
use tracing::info;

#[tokio::main]
async fn main() -> Result<()> {
// 1. Install the cloud-controlled file logger. Records land under
// `./logs/` and roll over hourly with up to 24 files kept. The
// WorkerGuard must outlive the process; drop it in `main`'s tail
// so any buffered records flush on exit.
let (collector, _guard) = logs::install(
FileLoggerConfig::new("./logs")
.with_rotation(Rotation::HOURLY)
.with_max_files(Some(24)),
)?;

// 2. Create the endpoint and parse the API secret so we know which
// cloud endpoint to grant SetLevel to.
let endpoint = Endpoint::bind(presets::N0).await?;
let secret = ApiSecret::from_env_var(API_SECRET_ENV_VAR_NAME)?;
let cloud_id = secret.addr().id;

let name = format!("logs-example-{}", &endpoint.id().to_string()[..8]);

// 3. Build the client. `with_log_collector(collector.clone())` makes
// the client pull the cloud's persisted directives right after
// auth and apply them locally — no waiting for the cloud to push.
let client = Client::builder(&endpoint)
.api_secret(secret)?
.name(name)?
.with_log_collector(collector.clone())
.build()
.await?;

// 4. Grant `LogsCap::SetLevel` so the dashboard can dial us back with
// live updates after the initial pull. Spawned so a momentarily-
// down cloud does not block startup.
let client_for_grant = client.clone();
let grant_task = tokio::spawn(async move {
if let Err(err) = client_for_grant
.grant_capability(cloud_id, [LogsCap::SetLevel])
.await
{
eprintln!("failed to grant LogsCap::SetLevel: {err:?}");
}
});

// 5. Accept the cloud's callback connections on `CLIENT_HOST_ALPN`.
// The `ClientHost` needs the same collector so dashboard-triggered
// overrides hot-reload the local filter.
let host = ClientHost::new(&endpoint).with_log_collector(collector);
let router = Router::builder(endpoint)
.accept(CLIENT_HOST_ALPN, host)
.spawn();

// 6. Emit an info log every other second forever. Records are written
// to the local rolling file once the cloud raises the level above
// `off`.
println!("emitting logs; ctrl+c to exit.");
let mut tick = tokio::time::interval(Duration::from_secs(2));
let mut counter: u64 = 0;
loop {
tokio::select! {
_ = tick.tick() => {
counter += 1;
info!(counter, "logs example heartbeat");
}
_ = tokio::signal::ctrl_c() => break,
}
}

grant_task.abort();
router.endpoint().close().await;
Ok(())
}
12 changes: 12 additions & 0 deletions examples/quickstart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,29 @@ use iroh_services::Client;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();

let endpoint = Endpoint::bind(presets::N0).await?;

// Wait for the endpoint to be online
endpoint.online().await;

// needs IROH_SERVICES_API_SECRET set to an environment variable
// client will now push endpoint metrics to iroh-services
let client = Client::builder(&endpoint)
.api_secret_from_env()?
.name("quickstart-example")?
.build()
.await?;

// we can also ping the service just to confirm everything is working
client.ping().await?;

// keep the endpoint running so it continues pushing metrics.
// ctrl+c to exit.
println!("endpoint running. ctrl+c to exit.");
tokio::signal::ctrl_c().await?;
endpoint.close().await;

Ok(())
}
31 changes: 31 additions & 0 deletions src/caps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ pub enum Cap {
Metrics(MetricsCap),
#[strum(to_string = "net-diagnostics:{0}")]
NetDiagnostics(NetDiagnosticsCap),
#[strum(to_string = "logs:{0}")]
Logs(LogsCap),
}

impl FromStr for Cap {
Expand All @@ -94,6 +96,7 @@ impl FromStr for Cap {
"metrics" => Self::Metrics(MetricsCap::from_str(inner)?),
"relay" => Self::Relay(RelayCap::from_str(inner)?),
"net-diagnostics" => Self::NetDiagnostics(NetDiagnosticsCap::from_str(inner)?),
"logs" => Self::Logs(LogsCap::from_str(inner)?),
_ => bail!("invalid cap domain"),
})
} else {
Expand Down Expand Up @@ -121,6 +124,19 @@ cap_enum!(
}
);

cap_enum!(
/// Capabilities for the log collection feature.
pub enum LogsCap {
/// Permits the bearer to push log lines to the cloud.
Push,
/// Permits the bearer to set the log level filter on the issuer at runtime.
SetLevel,
/// Permits the bearer to ask the issuer for the contents of its
/// local rolling log file.
Fetch,
}
);

impl Caps {
pub fn new(caps: impl IntoIterator<Item = impl Into<Cap>>) -> Self {
Self::V0(CapSet::new(caps))
Expand Down Expand Up @@ -179,6 +195,7 @@ impl Capability for Cap {
(Cap::Relay(slf), Cap::Relay(other)) => slf.permits(other),
(Cap::Metrics(slf), Cap::Metrics(other)) => slf.permits(other),
(Cap::NetDiagnostics(slf), Cap::NetDiagnostics(other)) => slf.permits(other),
(Cap::Logs(slf), Cap::Logs(other)) => slf.permits(other),
(_, _) => false,
}
}
Expand All @@ -192,6 +209,9 @@ fn client_capabilities(other: &Cap) -> bool {
Cap::Metrics(MetricsCap::PutAny) => true,
Cap::NetDiagnostics(NetDiagnosticsCap::PutAny) => true,
Cap::NetDiagnostics(NetDiagnosticsCap::GetAny) => true,
Cap::Logs(LogsCap::Push) => true,
Cap::Logs(LogsCap::SetLevel) => true,
Cap::Logs(LogsCap::Fetch) => true,
}
}

Expand Down Expand Up @@ -221,6 +241,17 @@ impl Capability for NetDiagnosticsCap {
}
}

impl Capability for LogsCap {
fn permits(&self, other: &Self) -> bool {
match (self, other) {
(LogsCap::Push, LogsCap::Push) => true,
(LogsCap::SetLevel, LogsCap::SetLevel) => true,
(LogsCap::Fetch, LogsCap::Fetch) => true,
(_, _) => false,
}
}
}

/// A set of capabilities
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize, Deserialize)]
pub struct CapSet<C: Capability + Ord>(BTreeSet<C>);
Expand Down
Loading
Loading