Skip to content

Commit f6a9fc0

Browse files
committed
feat(metrics-v3): add Datadog V3 payload encoder
Allows to encode metric payloads using V3 columnar format. This library enables users to encode their metric payloads using an efficient column-based protocol. For performance and compatibility reasons (we also want to keep this crate `no_std` so we could use it in very resource-constrained environments) this crate does manual protobuf serialization. Signed-off-by: Mark Kirichenko <mark.kirichenko@datadoghq.com>
1 parent e026a3c commit f6a9fc0

17 files changed

Lines changed: 3265 additions & 0 deletions

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ libdd-http-client @DataDog/apm-common-components-core
5555
libdd-agent-client @DataDog/apm-common-components-core
5656
libdd-library-config*/ @DataDog/apm-sdk-capabilities-rust
5757
libdd-log*/ @DataDog/apm-common-components-core
58+
libdd-metrics-v3/ @DataDog/libdatadog @DataDog/agent-metric-pipelines
5859
libdd-otel-thread-ctx/ @DataDog/apm-common-components-core
5960
libdd-otel-thread-ctx-ffi/ @DataDog/apm-common-components-core
6061
libdd-profiling*/ @DataDog/libdatadog-profiling
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env bash
2+
3+
# Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
set -eux
7+
8+
PROTO_FILE=""
9+
AGENT_PAYLOAD_COMMIT=""
10+
11+
while [[ $# -gt 0 ]]; do
12+
case $1 in
13+
--file)
14+
PROTO_FILE=$2
15+
shift && shift # past argument and value
16+
;;
17+
--commit)
18+
AGENT_PAYLOAD_COMMIT=$2
19+
shift && shift # past argument and value
20+
;;
21+
*)
22+
echo "Unknown option $1"
23+
exit 1
24+
;;
25+
esac
26+
done
27+
28+
if [ -z "$PROTO_FILE" ]; then
29+
echo "Missing --file argument"
30+
exit 1
31+
fi
32+
33+
if [ -z "$AGENT_PAYLOAD_COMMIT" ]; then
34+
echo "Missing --commit argument"
35+
exit 1
36+
fi
37+
38+
# Vendored files must stay byte-for-byte identical to their pinned commit in agent-payload from
39+
# `syntax = ...;` onward, so unlike diff-proto-files.sh (which fixes up import/package names for
40+
# datadog-agent's proto layout) this only strips the local "Vendored from:" preamble comment
41+
# before diffing, with no other rewriting.
42+
curl -sf "https://raw.githubusercontent.com/DataDog/agent-payload/$AGENT_PAYLOAD_COMMIT/proto/metrics/$PROTO_FILE" |
43+
diff -u <(sed -n '/^syntax = /,$p' "$PROTO_FILE") -
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: 'No-std check (libdd-metrics-v3)'
2+
on:
3+
pull_request:
4+
push:
5+
branches:
6+
- main
7+
- mq-working-branch-*
8+
env:
9+
CARGO_TERM_COLOR: always
10+
CARGO_INCREMENTAL: 0
11+
jobs:
12+
no-std-check:
13+
# `#![no_std]` alone only stops this crate's own code from referencing `std`; a dependency
14+
# that quietly requires std would still link fine on a normal host target. Building for a
15+
# target with no OS (so no std is even available to link against) is what actually proves the
16+
# whole dependency graph is no_std-compatible.
17+
name: "libdd-metrics-v3 builds for a target with no OS and no std"
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout sources
21+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
22+
- name: Read Rust version from rust-toolchain.toml
23+
id: rust-version
24+
run: echo "version=$(grep -Po '^channel = "\K[^"]+' rust-toolchain.toml)" >> $GITHUB_OUTPUT
25+
- name: Install ${{ steps.rust-version.outputs.version }} toolchain
26+
run: |
27+
rustup set profile minimal
28+
rustup install ${{ steps.rust-version.outputs.version }}
29+
rustup default ${{ steps.rust-version.outputs.version }}
30+
rustup target add x86_64-unknown-none
31+
rustup component add clippy
32+
- name: Cache [rust]
33+
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # 2.8.1
34+
with:
35+
cache-targets: true # cache build artifacts
36+
cache-bin: true # cache the ~/.cargo/bin directory
37+
- name: Build for x86_64-unknown-none
38+
run: cargo build -p libdd-metrics-v3 --target x86_64-unknown-none
39+
- name: Clippy for x86_64-unknown-none
40+
run: cargo clippy -p libdd-metrics-v3 --target x86_64-unknown-none -- -D warnings
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: 'Verify metrics-v3 proto'
2+
on:
3+
pull_request:
4+
types: [ opened, synchronize, reopened ]
5+
env:
6+
# Pinned commit of https://github.com/DataDog/agent-payload/blob/master/proto/metrics/intake_v3.proto
7+
AGENT_PAYLOAD_COMMIT: "512d523e386f75077efe9e8d878284104d491f46"
8+
CARGO_TERM_COLOR: always
9+
CARGO_INCREMENTAL: 0
10+
jobs:
11+
verify-proto-files:
12+
name: "Verify libdd-metrics-v3's vendored intake_v3.proto and generated bindings are in sync"
13+
runs-on: ubuntu-latest
14+
permissions:
15+
pull-requests: write
16+
steps:
17+
- name: Checkout sources
18+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
19+
- name: diff intake_v3.proto against agent-payload
20+
working-directory: libdd-metrics-v3/proto
21+
run: |
22+
../../.github/workflows/diff-agent-payload-proto.sh --file intake_v3.proto --commit "$AGENT_PAYLOAD_COMMIT"
23+
- name: Read Rust version from rust-toolchain.toml
24+
id: rust-version
25+
run: echo "version=$(grep -Po '^channel = "\K[^"]+' rust-toolchain.toml)" >> $GITHUB_OUTPUT
26+
- name: Install ${{ steps.rust-version.outputs.version }} toolchain
27+
run: rustup set profile minimal && rustup install ${{ steps.rust-version.outputs.version }} && rustup default ${{ steps.rust-version.outputs.version }}
28+
- name: Cache [rust]
29+
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # 2.8.1
30+
with:
31+
cache-targets: true # cache build artifacts
32+
cache-bin: true # cache the ~/.cargo/bin directory
33+
- name: diff tests/pb/mod.rs
34+
working-directory: libdd-metrics-v3
35+
run: |
36+
cargo build --features generate-protobuf
37+
git diff --exit-code -- tests/pb/mod.rs

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ members = [
6161
"libdd-log",
6262
"libdd-log-ffi",
6363
"libdd-sampling",
64+
"libdd-metrics-v3",
6465
]
6566

6667
# https://doc.rust-lang.org/cargo/reference/resolver.html

libdd-metrics-v3/Cargo.toml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
[package]
5+
name = "libdd-metrics-v3"
6+
version = "0.1.0"
7+
description = "V3 columnar protobuf codec for Datadog metrics"
8+
homepage = "https://github.com/DataDog/libdatadog/tree/main/libdd-metrics-v3"
9+
repository = "https://github.com/DataDog/libdatadog/tree/main/libdd-metrics-v3"
10+
edition.workspace = true
11+
rust-version.workspace = true
12+
license.workspace = true
13+
autobenches = false
14+
15+
[lints]
16+
workspace = true
17+
18+
[lib]
19+
bench = false
20+
21+
[dependencies]
22+
# default-features = false: enabling foldhash's default "std" feature would pull `std` back into
23+
# this otherwise `no_std` crate.
24+
foldhash = { version = "0.2", default-features = false }
25+
hashbrown = { version = "0.16", default-features = false }
26+
27+
[dev-dependencies]
28+
# Reference implementation used by the wire-format parity tests in `tests/`: encodes the same
29+
# columnar data via prost's generated bindings for `proto/intake_v3.proto` and checks that it's
30+
# byte-for-byte identical to this crate's hand-rolled encoder output.
31+
bolero = "0.13"
32+
prost = "0.14"
33+
34+
[build-dependencies]
35+
prost-build = { version = "0.14", optional = true }
36+
protoc-bin-vendored = { version = "3.0", optional = true }
37+
38+
[features]
39+
# Regenerates `tests/pb/mod.rs` from `proto/intake_v3.proto`. Not needed for normal builds or test
40+
# runs: the generated bindings are checked into version control, and CI verifies they're in sync
41+
# with the vendored .proto file (see `.github/workflows/verify-metrics-v3-proto.yml`).
42+
generate-protobuf = ["dep:prost-build", "dep:protoc-bin-vendored"]

libdd-metrics-v3/build.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// This build script always runs on the host with std available (the `no_std` library it builds
5+
// has nothing to do with the build script's own environment), so the workspace-wide
6+
// std-vs-core/alloc and expect-used lints (meant to guard the library itself) don't apply here.
7+
#![allow(
8+
clippy::std_instead_of_core,
9+
clippy::std_instead_of_alloc,
10+
clippy::expect_used
11+
)]
12+
13+
fn main() {
14+
#[cfg(feature = "generate-protobuf")]
15+
generate::run().expect("failed to regenerate protobuf bindings from proto/intake_v3.proto");
16+
#[cfg(not(feature = "generate-protobuf"))]
17+
println!("cargo:rerun-if-changed=build.rs");
18+
}
19+
20+
/// Regenerates `tests/pb/mod.rs` from `proto/intake_v3.proto`. Only compiled when the
21+
/// `generate-protobuf` feature is enabled, since it's the only fallible part of this build
22+
/// script: normal builds and test runs use the checked-in `tests/pb/mod.rs` and never reach it.
23+
#[cfg(feature = "generate-protobuf")]
24+
mod generate {
25+
use std::error::Error;
26+
use std::{env, fs, path::Path};
27+
28+
const HEADER: &str = "// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
29+
// SPDX-License-Identifier: Apache-2.0
30+
31+
// This file is @generated by prost-build from `proto/intake_v3.proto`. Do not edit it directly:
32+
// regenerate it with `cargo build -p libdd-metrics-v3 --features generate-protobuf` after
33+
// changing the vendored .proto file.
34+
#![allow(dead_code, clippy::all, clippy::pedantic, clippy::nursery)]
35+
36+
";
37+
38+
pub fn run() -> Result<(), Box<dyn Error>> {
39+
// protoc is required to compile proto files. This uses protoc-bin-vendored to obtain a
40+
// protoc binary, setting the env var to tell prost-build where to find it.
41+
env::set_var("PROTOC", protoc_bin_vendored::protoc_bin_path()?);
42+
43+
let cur_working_dir = env::var("CARGO_MANIFEST_DIR")?;
44+
let out_dir = Path::new(&cur_working_dir).join("tests/pb");
45+
fs::create_dir_all(&out_dir)?;
46+
47+
let mut config = prost_build::Config::new();
48+
config.out_dir(&out_dir);
49+
50+
println!("cargo:rerun-if-changed=proto/intake_v3.proto");
51+
config.compile_protos(&["proto/intake_v3.proto"], &["proto"])?;
52+
53+
// prost-build names the output file after the proto's package
54+
// (`datadoghq.api.metrics.v3.rs`); rename it to `mod.rs` so `tests/parity.rs` can
55+
// pull it in as a plain submodule.
56+
let generated = out_dir.join("datadoghq.api.metrics.v3.rs");
57+
let mod_rs = out_dir.join("mod.rs");
58+
fs::rename(&generated, &mod_rs)?;
59+
60+
prepend_to_file(HEADER.as_bytes(), &mod_rs)?;
61+
62+
Ok(())
63+
}
64+
65+
fn prepend_to_file(data: &[u8], file_path: &Path) -> Result<(), Box<dyn Error>> {
66+
use std::io::{Read, Write};
67+
68+
let mut f = fs::File::open(file_path)?;
69+
let mut content = data.to_owned();
70+
f.read_to_end(&mut content)?;
71+
72+
let mut f = fs::File::create(file_path)?;
73+
f.write_all(content.as_slice())?;
74+
Ok(())
75+
}
76+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Vendored from:
2+
// https://github.com/DataDog/agent-payload/blob/512d523e386f75077efe9e8d878284104d491f46/proto/metrics/intake_v3.proto
3+
//
4+
// This is the wire format that `libdd-metrics-v3`'s hand-rolled encoder (see `src/writer.rs`)
5+
// implements without a Protocol Buffers library. `.github/workflows/verify-metrics-v3-proto.yml`
6+
// checks in CI that the content below stays byte-for-byte identical to the pinned commit above,
7+
// and `tests/parity.rs` checks that our hand-rolled encoder's output is byte-identical to what
8+
// prost's generated bindings for this file produce from the same data.
9+
10+
syntax = "proto3";
11+
12+
package datadoghq.api.metrics.v3;
13+
14+
option go_package = "github.com/DataDog/agent-payload/v5/metrics/intake_v3";
15+
16+
message Payload {
17+
reserved 1; // for compatibility with agentpayload.MetricPayload.series
18+
Metadata metadata = 2;
19+
MetricData metricData = 3;
20+
}
21+
22+
message Metadata {
23+
repeated string tags = 1;
24+
repeated string resources = 2; // even number of elements, [Type, Name] pairs
25+
}
26+
27+
message MetricData {
28+
// Dictionaries
29+
// All dictionary indexes are base-1, zero implicitly represents an empty value.
30+
bytes dictNameStr = 1; // varint length + value
31+
bytes dictTagStr = 2; // varint length + value
32+
repeated sint64 dictTagsets = 3; // length, delta encoded set of indexes into dictTagsStr
33+
34+
bytes dictResourceStr = 4; // varint length + value
35+
repeated int64 dictResourceLen = 5; // number of elements in Type and Name arrays
36+
repeated sint64 dictResourceType = 6; // delta encoded set of indexes into dictResourceStr
37+
repeated sint64 dictResourceName = 7; // delta encoded set of indexes into dictResourceStr
38+
39+
bytes dictSourceTypeName = 8; // varint length + value
40+
repeated int32 dictOriginInfo = 9; // (product, category, service) tuples
41+
bytes dictUnitStr = 25; // varint length + value
42+
43+
// One entry per time series
44+
repeated uint64 types = 10; // type = metricType | valueType | metricFlags
45+
repeated sint64 nameRefs = 11; // index into dictNameStr, entire array is delta encoded
46+
repeated sint64 tagsetRefs = 12; // index into dictTagsets, entire array is delta encoded
47+
repeated sint64 resourcesRefs = 13; // index into dictResourceLen, entire array is delta encoded
48+
repeated uint64 intervals = 14;
49+
repeated uint64 numPoints = 15;
50+
repeated sint64 sourceTypeNameRefs = 23; // index into dictSourceTypeName, entire array is delta encoded
51+
repeated sint64 originInfoRefs = 24; // index into dictOriginInfo, entire array is delta encoded
52+
repeated sint64 unitRefs = 26; // index into dictUnitStr, value present if flagHasUnit is set, entire array is delta encoded
53+
54+
// each metric has numPoints values in this section
55+
repeated sint64 timestamps = 16; // entire array delta encoded
56+
repeated sint64 valsSint64 = 17; // or
57+
repeated float valsFloat32 = 18; // or
58+
repeated double valsFloat64 = 19; // based on valueType
59+
repeated uint64 sketchNumBins = 20;
60+
repeated sint32 sketchBinKeys = 21; // per-metric sequence is delta encoded
61+
repeated uint32 sketchBinCnts = 22;
62+
// sketch summary Sum, Min, Max are encoded as three consecutive elements in one of vals using valueType
63+
// sketch summary Cnt is always encoded in valInt64
64+
// sketch summary Avg is reconstructed as Sum/Cnt in the intake
65+
}
66+
67+
enum metricType {
68+
UNUSED = 0;
69+
Count = 1;
70+
Rate = 2;
71+
Gauge = 3;
72+
Sketch = 4;
73+
}
74+
75+
enum valueType {
76+
Zero = 0x00; // value is zero, not stored explicitly
77+
Sint64 = 0x10; // value is stored in valsSint64
78+
Float32 = 0x20; // value is stored in valsFloat32
79+
Float64 = 0x30; // value is stored in valsFloat64
80+
}
81+
82+
enum metricFlags {
83+
flagNone = 0;
84+
flagNoIndex = 0x100; // metric should not be indexed (equivalent to origin metric type == agent_hidden in v2)
85+
flagHasUnit = 0x200; // timeseries has a unit in the unitRefs column
86+
}
87+
88+
message Response {
89+
string error = 1;
90+
}

0 commit comments

Comments
 (0)