Skip to content
This repository has been archived by the owner on Jun 28, 2022. It is now read-only.

Commit

Permalink
Cleanup slices, strings, tags, and vecs (#44)
Browse files Browse the repository at this point in the history
This started with enhancing tags, but when I was done I discovered a
sigsev which could be triggered even from Rust code. I knew I had
screwed up the lifetimes and unsafe code.

So, I started working and I kept unravelling more and more.

Slice and vec have been split into their own files as lib.rs was
getting large.

Many From traits were changed to work with &T instead of T because of
lifetime issues.

In this PR, some C FFI APIs for tags have been removed. A subsequent PR
will add them back and enhance them. I wanted to keep the PR size to be
somewhat manageable.

Some places using some form of string have changed to use
`Cow<'static, str>`. This allows you to borrow static strings, and own
all others. When calling from C, the difference is very little because
they _should_ have been copied (probably, was unsafe if not). I believe
these are the changes which actually fixed the crash.
  • Loading branch information
morrisonlevi committed Apr 12, 2022
1 parent e1bd12d commit 9989036
Show file tree
Hide file tree
Showing 8 changed files with 593 additions and 614 deletions.
41 changes: 22 additions & 19 deletions ddprof-exporter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ use tokio::runtime::Runtime;
mod connector;
mod container_id;
mod errors;
pub mod tag;

pub use tag::*;

#[cfg(unix)]
pub use connector::uds::socket_path_to_uri;
Expand All @@ -30,26 +33,21 @@ pub struct Exporter {
runtime: Runtime,
}

pub struct Tag {
pub name: Cow<'static, str>,
pub value: Cow<'static, str>,
}

pub struct FieldsV3 {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}

pub struct Endpoint {
url: Uri,
api_key: Option<String>,
api_key: Option<Cow<'static, str>>,
}

pub struct ProfileExporterV3 {
exporter: Exporter,
endpoint: Endpoint,
family: String,
tags: Vec<Tag>,
family: Cow<'static, str>,
tags: Option<Vec<Tag>>,
}

pub struct Request {
Expand Down Expand Up @@ -139,20 +137,23 @@ impl Endpoint {
/// # Arguments
/// * `site` - e.g. "datadoghq.com".
/// * `api_key`
pub fn agentless<S: AsRef<str>>(site: S, api_key: S) -> Result<Endpoint, Box<dyn Error>> {
let intake_url = format!("https://intake.profile.{}/v1/input", site.as_ref());
pub fn agentless<AsStrRef: AsRef<str>, IntoCow: Into<Cow<'static, str>>>(
site: AsStrRef,
api_key: IntoCow,
) -> Result<Endpoint, Box<dyn Error>> {
let intake_url: String = format!("https://intake.profile.{}/v1/input", site.as_ref());

Ok(Endpoint {
url: Uri::from_str(intake_url.as_str())?,
api_key: Some(String::from(api_key.as_ref())),
api_key: Some(api_key.into()),
})
}
}

impl ProfileExporterV3 {
pub fn new<S: Into<String>>(
family: S,
tags: Vec<Tag>,
pub fn new<IntoCow: Into<Cow<'static, str>>>(
family: IntoCow,
tags: Option<Vec<Tag>>,
endpoint: Endpoint,
) -> Result<ProfileExporterV3, Box<dyn Error>> {
Ok(Self {
Expand All @@ -169,18 +170,20 @@ impl ProfileExporterV3 {
start: chrono::DateTime<chrono::Utc>,
end: chrono::DateTime<chrono::Utc>,
files: &[File],
additional_tags: &[Tag],
additional_tags: Option<&Vec<Tag>>,
timeout: std::time::Duration,
) -> Result<Request, Box<dyn Error>> {
let mut form = multipart::Form::default();

form.add_text("version", "3");
form.add_text("start", start.format("%Y-%m-%dT%H:%M:%S%.9fZ").to_string());
form.add_text("end", end.format("%Y-%m-%dT%H:%M:%S%.9fZ").to_string());
form.add_text("family", String::from(&self.family));
form.add_text("family", self.family.to_owned());

for tag in self.tags.iter().chain(additional_tags.iter()) {
form.add_text("tags[]", format!("{}:{}", tag.name, tag.value));
for tags in self.tags.as_ref().iter().chain(additional_tags.iter()) {
for tag in tags.iter() {
form.add_text("tags[]", tag.to_string());
}
}

for file in files {
Expand All @@ -200,7 +203,7 @@ impl ProfileExporterV3 {
if let Some(api_key) = &self.endpoint.api_key {
builder = builder.header(
"DD-API-KEY",
HeaderValue::from_str(api_key.as_str()).expect("TODO"),
HeaderValue::from_str(api_key).expect("Error setting api_key"),
);
}

Expand Down
120 changes: 120 additions & 0 deletions ddprof-exporter/src/tag.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use std::borrow::Cow;
use std::fmt::{Debug, Display, Formatter};

#[derive(Clone, Eq, PartialEq)]
pub struct Tag {
key: Cow<'static, str>,
value: Cow<'static, str>,
}

impl Debug for Tag {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tag")
.field("key", &self.key)
.field("value", &self.value)
.finish()
}
}

// Any type which implements Display automatically has to_string.
impl Display for Tag {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// A tag isn't supposed to end with a colon, so if there isn't a value
// then don't follow the tag with a colon.
if self.value.is_empty() {
write!(f, "{}", self.key)
} else {
write!(f, "{}:{}", self.key, self.value)
}
}
}

impl Tag {
pub fn new<IntoCow: Into<Cow<'static, str>>>(
key: IntoCow,
value: IntoCow,
) -> Result<Self, Cow<'static, str>> {
let key = key.into();
let value = value.into();
if key.is_empty() {
return Err("tag key was empty".into());
}

let first_valid_char = key
.chars()
.find(|char| *char != std::char::REPLACEMENT_CHARACTER && !char.is_whitespace());

if first_valid_char.is_none() {
return Err("tag contained only whitespace or invalid unicode characters".into());
}

Ok(Self { key, value })
}

pub fn key(&self) -> &Cow<str> {
&self.key
}
pub fn value(&self) -> &Cow<str> {
&self.value
}

pub fn into_owned(mut self) -> Self {
self.key = self.key.to_owned();
self.value = self.value.to_owned();
self
}
}

#[cfg(test)]
mod tests {
use crate::Tag;

#[test]
fn test_empty_key() {
let _ = Tag::new("", "woof").expect_err("empty key is not allowed");
}

#[test]
fn test_empty_value() {
let tag = Tag::new("key1", "").expect("empty value is okay");
assert_eq!("key1", tag.to_string()); // notice no trailing colon!
}

#[test]
fn test_bad_utf8() {
// 0b1111_0xxx is the start of a 4-byte sequence, but there aren't any
// more chars, so it will get converted into the utf8 replacement
// character. This results in a string with a space (32) and a
// replacement char, so it should be an error (no valid chars).
let bytes = &[32, 0b1111_0111];
let key = String::from_utf8_lossy(bytes);
let _ = Tag::new(key, "value".into()).expect_err("invalid tag is rejected");
}

#[test]
fn test_value_has_colon() {
let result = Tag::new("env", "staging:east").expect("values can have colons");
assert_eq!("env:staging:east", result.to_string());
}

#[test]
fn test_suspicious_tags() {
// Based on tag rules, these should all fail. However, there is a risk
// that profile tags will then differ or cause failures compared to
// trace tags. These require cross-team, cross-language collaboration.
let cases = [
(":key-starts-with-colon".to_string(), "value".to_owned()),
("key".to_string(), "value-ends-with-colon:".to_owned()),
(
"the-tag-length-is-over-200-characters".repeat(6),
"value".to_owned(),
),
];

for case in cases {
let result = Tag::new(case.0, case.1);
// Again, these should fail, but it's not implemented yet
assert!(result.is_ok())
}
}
}
108 changes: 54 additions & 54 deletions ddprof-exporter/tests/form.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021-Present Datadog, Inc.

use ddprof_exporter::{Endpoint, File, ProfileExporterV3, Request, Tag};
use std::borrow::Cow;
use ddprof_exporter::{File, ProfileExporterV3, Request};
use std::error::Error;
use std::io::Read;
use std::ops::Sub;
Expand Down Expand Up @@ -33,63 +32,64 @@ fn multipart(exporter: &ProfileExporterV3) -> Request {
let timeout = std::time::Duration::from_secs(10);

let request = exporter
.build(start, end, files, &[], timeout)
.build(start, end, files, None, timeout)
.expect("request to be built");

let actual_timeout = request.timeout().expect("timeout to exist");
assert_eq!(actual_timeout, timeout);
request
}

fn default_tags() -> Vec<Tag> {
vec![
Tag {
name: Cow::Borrowed("service"),
value: Cow::Borrowed("php"),
},
Tag {
name: Cow::Borrowed("host"),
value: Cow::Borrowed("bits"),
},
]
}

#[test]
fn multipart_agent() {
let base_url = "http://localhost:8126".parse().expect("url to parse");
let endpoint = Endpoint::agent(base_url).expect("endpoint to construct");
let exporter =
ProfileExporterV3::new("php", default_tags(), endpoint).expect("exporter to construct");

let request = multipart(&exporter);

assert_eq!(
request.uri().to_string(),
"http://localhost:8126/profiling/v1/input"
);

let actual_headers = request.headers();
assert!(!actual_headers.contains_key("DD-API-KEY"));
}

#[test]
fn multipart_agentless() {
let api_key = "1234567890123456789012";
let endpoint = Endpoint::agentless("datadoghq.com", api_key).expect("endpoint to construct");
let exporter =
ProfileExporterV3::new("php", default_tags(), endpoint).expect("exporter to construct");

let request = multipart(&exporter);

assert_eq!(
request.uri().to_string(),
"https://intake.profile.datadoghq.com/v1/input"
);

let actual_headers = request.headers();

assert_eq!(
actual_headers.get("DD-API-KEY").expect("api key to exist"),
api_key
);
#[cfg(test)]
mod tests {
use crate::multipart;
use ddprof_exporter::*;

fn default_tags() -> Vec<Tag> {
vec![
Tag::new("service", "php").expect("static tags to be valid"),
Tag::new("host", "bits").expect("static tags to be valid"),
]
}

#[test]
fn multipart_agent() {
let base_url = "http://localhost:8126".parse().expect("url to parse");
let endpoint = Endpoint::agent(base_url).expect("endpoint to construct");
let exporter = ProfileExporterV3::new("php", Some(default_tags()), endpoint)
.expect("exporter to construct");

let request = multipart(&exporter);

assert_eq!(
request.uri().to_string(),
"http://localhost:8126/profiling/v1/input"
);

let actual_headers = request.headers();
assert!(!actual_headers.contains_key("DD-API-KEY"));
}

#[test]
fn multipart_agentless() {
let api_key = "1234567890123456789012";
let endpoint =
Endpoint::agentless("datadoghq.com", api_key).expect("endpoint to construct");
let exporter = ProfileExporterV3::new("php", Some(default_tags()), endpoint)
.expect("exporter to construct");

let request = multipart(&exporter);

assert_eq!(
request.uri().to_string(),
"https://intake.profile.datadoghq.com/v1/input"
);

let actual_headers = request.headers();

assert_eq!(
actual_headers.get("DD-API-KEY").expect("api key to exist"),
api_key
);
}
}
Loading

0 comments on commit 9989036

Please sign in to comment.