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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ and this project adheres to

### Added

- [#5983](https://github.com/firecracker-microvm/firecracker/pull/5983): Add two
optional metrics fields, set via `PUT /metrics` or a config file: `emit_id`
emits the microVM instance id under a top-level `id` field, and `properties`
emits operator-defined key-value pairs under a top-level `properties` field.
Each is opt-in and independent. See [metrics documentation](docs/metrics.md).

### Changed

### Deprecated
Expand Down
73 changes: 73 additions & 0 deletions docs/metrics.md
Comment thread
JamesC1305 marked this conversation as resolved.
Comment thread
JamesC1305 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,79 @@ Details about this configuration can be found in the

The metrics are written to the `metrics_path` in JSON format.

## Optional metrics fields

Firecracker can emit additional top-level fields on every metrics line. Each is
opt-in and off by default, so the default output is unchanged. They are
configured alongside `metrics_path`, through the `PUT /metrics` API request or
the `metrics` block of a configuration file; the `--metrics-path` CLI option
configures only the path. Like the rest of the metrics configuration, they are
set once before boot and fixed for the lifetime of the microVM.

### `id`

Set `emit_id` to `true` to emit the microVM instance id (the value passed to
`--id`, defaulting to `anonymous-instance`) under a top-level `id` field. This
lets lines collected from multiple microVMs into one destination be attributed
to their source.

### `properties`

Set `properties` to a map of operator-defined key-value pairs to emit them under
a top-level `properties` field. The map is bounded:

- up to 10 entries
- keys up to 64 bytes
- values up to 512 bytes

### Examples

Configure the fields via the API by adding them to the request body:

```bash
curl --unix-socket /tmp/firecracker.socket -i \
-X PUT "http://localhost/metrics" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d "{
\"metrics_path\": \"metrics.fifo\",
\"emit_id\": true,
\"properties\": {
\"customer_id\": \"1234\",
\"bundle_id\": \"fn-abc\"
}
}"
```

The same fields can be provided in the `metrics` block of a configuration file
passed via `--config-file`:

```json
{
"metrics": {
"metrics_path": "metrics.fifo",
"emit_id": true,
"properties": {
"customer_id": "1234",
"bundle_id": "fn-abc"
}
}
}
```

With neither field configured, a line carries only the default keys:

```json
{"utc_timestamp_ms": 1739000000000, "api_server": {"...": 0}}
```

With both fields configured as above, the same line also carries the instance id
and the properties:

```json
{"utc_timestamp_ms": 1739000000000, "id": "my-instance", "properties": {"bundle_id": "fn-abc", "customer_id": "1234"}, "api_server": {"...": 0}}
```

## Flushing the metrics

The metrics get flushed in two ways:
Expand Down
42 changes: 42 additions & 0 deletions src/firecracker/src/api_server/request/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ mod tests {
}"#;
let expected_config = MetricsConfig {
metrics_path: PathBuf::from("metrics"),
emit_id: false,
properties: None,
};
assert_eq!(
vmm_action_from_request(parse_put_metrics(&Body::new(body)).unwrap()),
Expand All @@ -42,4 +44,44 @@ mod tests {
}"#;
parse_put_metrics(&Body::new(invalid_body)).unwrap_err();
}

#[test]
fn test_parse_put_metrics_request_with_emit_id() {
let body = r#"{
"metrics_path": "metrics",
"emit_id": true
}"#;
let expected_config = MetricsConfig {
metrics_path: PathBuf::from("metrics"),
emit_id: true,
properties: None,
};
assert_eq!(
vmm_action_from_request(parse_put_metrics(&Body::new(body)).unwrap()),
VmmAction::ConfigureMetrics(expected_config)
);
}

#[test]
fn test_parse_put_metrics_request_with_properties() {
let body = r#"{
"metrics_path": "metrics",
"properties": {
"customer_id": "1234",
"bundle_id": "fn-abc"
}
}"#;
let mut properties = std::collections::BTreeMap::new();
properties.insert("customer_id".to_string(), "1234".to_string());
properties.insert("bundle_id".to_string(), "fn-abc".to_string());
let expected_config = MetricsConfig {
metrics_path: PathBuf::from("metrics"),
emit_id: false,
properties: Some(properties),
};
assert_eq!(
vmm_action_from_request(parse_put_metrics(&Body::new(body)).unwrap()),
VmmAction::ConfigureMetrics(expected_config)
);
}
}
2 changes: 2 additions & 0 deletions src/firecracker/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ fn main_exec() -> Result<(), MainError> {
if let Some(metrics_path) = arguments.single_value("metrics-path") {
let metrics_config = MetricsConfig {
metrics_path: PathBuf::from(metrics_path),
emit_id: false,
properties: None,
};
init_metrics(metrics_config).map_err(MainError::MetricsInitialization)?;
}
Expand Down
14 changes: 14 additions & 0 deletions src/firecracker/swagger/firecracker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,20 @@ definitions:
metrics_path:
type: string
description: Path to the named pipe or file where the JSON-formatted metrics are flushed.
emit_id:
type: boolean
description:
Whether to emit the microVM instance id (the value passed to the
jailer "--id") as a top-level "id" field on each metrics line.
default: false
properties:
type: object
description:
Operator-defined key-value pairs emitted under a top-level
"properties" field on each metrics line. At most 10 entries, with
keys up to 64 bytes and values up to 512 bytes.
additionalProperties:
type: string

MmdsConfig:
type: object
Expand Down
143 changes: 141 additions & 2 deletions src/vmm/src/logger/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
//! named `block` which is in turn a serializable child structure collecting metrics for
//! the block device such as `activate_fails`, `cfg_fails`, etc.
//!
//! Besides the per-component metrics, two top-level fields can be emitted, controlled
//! independently via the metrics API or config file: `id`, the microVM instance id (the jailer
//! `--id`), enabled by `emit_id`; and `properties`, a map of operator-defined key-value pairs.
//! See the `InstanceIdField` and `MetricsProperties` structs.
//!
//! # Limitations
//! Metrics are only written to buffers.
//!
Expand Down Expand Up @@ -61,16 +66,18 @@
//! If if turns out this approach is not really what we want, it's pretty easy to resort to
//! something else, while working behind the same interface.

use std::collections::BTreeMap;
use std::fmt::Debug;
use std::io::Write;
use std::ops::Deref;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock};

use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
use utils::time::{ClockType, get_time_ns, get_time_us};

use super::FcLineWriter;
use super::{DEFAULT_INSTANCE_ID, FcLineWriter, INSTANCE_ID};
use crate::devices::legacy;
use crate::devices::virtio::balloon::metrics as balloon_metrics;
use crate::devices::virtio::block::virtio::metrics as block_metrics;
Expand Down Expand Up @@ -332,6 +339,40 @@ impl ProcessTimeReporter {
// are interested in. Whenever the name of a field differs from its ideal textual representation
// in the serialized form, we can use the #[serde(rename = "name")] attribute to, well, rename it.

/// Operator-supplied key-value pairs emitted alongside the metrics.
#[derive(Debug, Default)]
pub struct MetricsProperties {
/// The properties, set once at metrics configuration time.
inner: OnceLock<BTreeMap<String, String>>,
Comment thread
JamesC1305 marked this conversation as resolved.
}

impl Serialize for MetricsProperties {
/// Serializes as a flattened `properties` field when set, or as nothing when unset.
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
if let Some(props) = self.inner.get() {
map.serialize_entry("properties", props)?;
}
map.end()
}
}

impl MetricsProperties {
/// Const default construction.
pub const fn new() -> Self {
Self {
inner: OnceLock::new(),
}
}

/// Sets the properties once. Returns an error if already set.
pub fn set(&self, properties: BTreeMap<String, String>) -> Result<(), MetricsError> {
self.inner
.set(properties)
.map_err(|_| MetricsError::AlreadyInitialized)
}
}

/// Metrics related to the internal API server.
#[derive(Debug, Default, Serialize)]
pub struct ApiServerMetrics {
Expand Down Expand Up @@ -873,6 +914,48 @@ impl Serialize for SerializeToUtcTimestampMs {
}
}

/// The microVM instance id emitted alongside the metrics.
#[derive(Debug, Default)]
pub struct InstanceIdField {
/// Whether the `id` field should be emitted. Flipped once at metrics configuration time.
enabled: AtomicBool,
Comment thread
JamesC1305 marked this conversation as resolved.
}

impl Serialize for InstanceIdField {
/// Serializes as a flattened `id` field (the jailer `--id`) when enabled, or as nothing when
/// disabled.
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
if self.enabled.load(Ordering::Relaxed) {
map.serialize_entry(
"id",
INSTANCE_ID
.get()
.map(String::as_str)
.unwrap_or(DEFAULT_INSTANCE_ID),
)?;
}
map.end()
}
}

impl InstanceIdField {
/// Const default construction.
pub const fn new() -> Self {
Self {
enabled: AtomicBool::new(false),
}
}

/// Enables emission of the `id` field. Returns an error if already enabled.
pub fn enable(&self) -> Result<(), MetricsError> {
if self.enabled.swap(true, Ordering::Relaxed) {
Comment thread
JamesC1305 marked this conversation as resolved.
return Err(MetricsError::AlreadyInitialized);
}
Ok(())
}
}

macro_rules! create_serialize_proxy {
// By using the below structure in FirecrackerMetrics it is easy
// to serialise Firecracker app_metrics as a single json object which
Expand Down Expand Up @@ -907,6 +990,12 @@ create_serialize_proxy!(MemoryHotplugSerializeProxy, virtio_mem_metrics);
#[derive(Debug, Default, Serialize)]
pub struct FirecrackerMetrics {
utc_timestamp_ms: SerializeToUtcTimestampMs,
#[serde(flatten)]
/// The microVM instance id (jailer `--id`), emitted when enabled.
pub id: InstanceIdField,
#[serde(flatten)]
/// Operator-supplied custom properties.
pub properties: MetricsProperties,
/// API Server related metrics.
pub api_server: ApiServerMetrics,
#[serde(flatten)]
Expand Down Expand Up @@ -966,6 +1055,8 @@ impl FirecrackerMetrics {
pub const fn new() -> Self {
Self {
utc_timestamp_ms: SerializeToUtcTimestampMs::new(),
id: InstanceIdField::new(),
properties: MetricsProperties::new(),
api_server: ApiServerMetrics::new(),
balloon_ser: BalloonMetricsSerializeProxy {},
block_ser: BlockMetricsSerializeProxy {},
Expand Down Expand Up @@ -1028,6 +1119,54 @@ mod tests {
m.init(LineWriter::new(f.into_file())).unwrap_err();
}

#[test]
fn test_instance_id_serialize_disabled() {
let id = InstanceIdField::new();
assert_eq!(serde_json::to_string(&id).unwrap(), "{}");
}

#[test]
fn test_instance_id_serialize_enabled() {
let id = InstanceIdField::new();
id.enable().unwrap();
let out = serde_json::to_string(&id).unwrap();
assert!(out.contains(r#""id":"#));
}

#[test]
fn test_instance_id_enable_once() {
let id = InstanceIdField::new();
id.enable().unwrap();
id.enable().unwrap_err();
}

#[test]
fn test_metrics_properties_serialize_unset() {
let props = MetricsProperties::new();
assert_eq!(serde_json::to_string(&props).unwrap(), "{}");
}

#[test]
fn test_metrics_properties_serialize_set() {
let props = MetricsProperties::new();
let mut map = BTreeMap::new();
map.insert("customer_id".to_string(), "1234".to_string());
map.insert("bundle_id".to_string(), "fn-abc".to_string());
props.set(map).unwrap();

assert_eq!(
serde_json::to_string(&props).unwrap(),
r#"{"properties":{"bundle_id":"fn-abc","customer_id":"1234"}}"#
);
}

#[test]
fn test_metrics_properties_set_once() {
let props = MetricsProperties::new();
props.set(BTreeMap::new()).unwrap();
props.set(BTreeMap::new()).unwrap_err();
}

#[test]
fn test_shared_inc_metric() {
let metric = Arc::new(SharedIncMetric::default());
Expand Down
2 changes: 2 additions & 0 deletions src/vmm/src/rpc_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1289,6 +1289,8 @@ mod tests {
check_unsupported(runtime_request(VmmAction::ConfigureMetrics(
MetricsConfig {
metrics_path: PathBuf::new(),
emit_id: false,
properties: None,
},
)));
check_unsupported(runtime_request(VmmAction::SetVsockDevice(
Expand Down
Loading
Loading