Skip to content

Commit

Permalink
TraceState implementation as per spec (#551)
Browse files Browse the repository at this point in the history
  • Loading branch information
lalitb committed Feb 4, 2021
1 parent 74ad7b1 commit ea87130
Show file tree
Hide file tree
Showing 3 changed files with 346 additions and 90 deletions.
277 changes: 244 additions & 33 deletions api/include/opentelemetry/trace/trace_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@

#include <cstdint>
#include <cstring>
#include <string>

#include <regex>
#if (__GNUC__ == 4 && (__GNUC_MINOR__ == 8))
# define HAVE_WORKING_REGEX 0
#else
# define HAVE_WORKING_REGEX 1
#endif

#include "opentelemetry/nostd/span.h"
#include "opentelemetry/nostd/string_view.h"
Expand All @@ -36,9 +44,11 @@ namespace trace
class TraceState
{
public:
static constexpr int kKeyMaxSize = 256;
static constexpr int kValueMaxSize = 256;
static constexpr int kMaxKeyValuePairs = 32;
static constexpr int kKeyMaxSize = 256;
static constexpr int kValueMaxSize = 256;
static constexpr int kMaxKeyValuePairs = 32;
static constexpr auto kKeyValueSeparator = '=';
static constexpr auto kMembersSeparator = ',';

// Class to store key-value pairs.
class Entry
Expand Down Expand Up @@ -97,37 +107,174 @@ class TraceState
}
};

// An empty TraceState.
TraceState() noexcept : entries_(new Entry[kMaxKeyValuePairs]), num_entries_(0) {}
/**
* Returns a newly created TraceState parsed from the header provided.
* @param header Encoding of the tracestate header defined by
* the W3C Trace Context specification https://www.w3.org/TR/trace-context/
* @return Tracestate A new TraceState instance or DEFAULT
*/
static TraceState FromHeader(nostd::string_view header)
{
TraceState ts;

std::size_t begin{0};
std::size_t end{0};
bool invalid_header = false;
while (begin < header.size() && ts.num_entries_ < kMaxKeyValuePairs)
{
// find list-member
end = header.find(kMembersSeparator, begin);
if (end == 0)
{
// special case where "," is first char, move to next list member
begin = 1;
continue;
}
if (end == std::string::npos)
{
// last list member. `end` points to end of it.
end = header.size() - 1;
}
else
{
// `end` points to end of current list member
end--;
}
auto list_member = TrimString(header, begin, end); // OWS handling
if (list_member.size() == 0)
{
// empty list member, move to next in list
begin = end + 2; // begin points to start of next member
continue;
}
auto key_end_pos = list_member.find(kKeyValueSeparator);
if (key_end_pos == std::string::npos)
{
// Error: invalid list member, return empty TraceState
ts.entries_.reset(nullptr);
ts.num_entries_ = 0;
break;
}
auto key = list_member.substr(0, key_end_pos);
auto value = list_member.substr(key_end_pos + 1);
if (!IsValidKey(key) || !IsValidValue(value))
{
// invalid header. return empty TraceState
ts.entries_.reset(nullptr);
ts.num_entries_ = 0;
break;
}
Entry entry(key, value);
(ts.entries_.get())[ts.num_entries_] = entry;
ts.num_entries_++;

begin = end + 2;
}
return ts;
}

/**
* Creates a w3c tracestate header from TraceState object
*/
std::string ToHeader()
{
std::string header_s;
for (size_t count = 0; count < num_entries_; count++)
{
if (count != 0)
{
header_s.append(",");
}

// Returns false if no such key, otherwise returns true and populates the value parameter with the
// associated value.
bool Get(nostd::string_view key, nostd::string_view &value) const noexcept
auto entry = (entries_.get())[count];
header_s.append(std::string(entry.GetKey()));
header_s.append(1, kKeyValueSeparator);
header_s.append(std::string(entry.GetValue()));
}
return header_s;
}

/**
* Returns `value` associated with `key` passed as argument
* Returns empty string if key is invalid or not found
*/
std::string Get(nostd::string_view key) const noexcept
{
for (const auto &entry : Entries())
if (!IsValidKey(key))
{
return std::string();
}
for (size_t i = 0; i < num_entries_; i++)
{
auto entry = (entries_.get())[i];
if (key == entry.GetKey())
{
value = entry.GetValue();
return true;
return std::string(entry.GetValue());
}
}
return false;
return std::string();
}

// Creates an Entry for the key-value pair and adds it to entries. Returns true if pair was added
// successfully, false otherwise. If value is null or entries_ is full, this function is a no-op.
bool Set(nostd::string_view key, nostd::string_view value) noexcept
/**
* Returns `new` TraceState object with following mutations applied to the existing instance:
* Update Key value: The updated value must be moved to beginning of List
* Add : The new key-value pair SHOULD be added to beginning of List
*
* If the provided key-value pair is invalid, or results in transtate that violates the
* tracecontext specification, empty TraceState instance will be returned.
*
* If the existing object has maximum list members, it's copy is returned.
*/
TraceState Set(const nostd::string_view &key, const nostd::string_view &value)
{
if (value.empty() || num_entries_ >= kMaxKeyValuePairs)
TraceState ts;
if ((!IsValidKey(key) || !IsValidValue(value)))
{
return false;
// max size reached or invalid key/value. Returning empty TraceState
return ts; // empty instance
}

Entry entry(key, value);
(entries_.get())[num_entries_] = entry;
num_entries_++;
return true;
// add new key-value pair at beginning if list is not reached to its limit
if (num_entries_ < kMaxKeyValuePairs)
{
Entry e(key, value);
(ts.entries_.get())[ts.num_entries_++] = e;
}
for (size_t i = 0; i < num_entries_; i++)
{
auto entry = (entries_.get())[i];
auto key = entry.GetKey();
auto value = entry.GetValue();
Entry e(key, value);
(ts.entries_.get())[ts.num_entries_++] = e;
}
return ts;
}

/**
* Returns `new` TraceState object after removing the attribute with given key ( if present )
* @returns empty TraceState object if key is invalid
* @returns copy of original TraceState object if key is not present (??)
*/
TraceState Delete(const nostd::string_view &key)
{
TraceState ts;
if (!IsValidKey(key))
{
return ts;
}
for (size_t i = 0; i < num_entries_; i++)
{
auto entry = (entries_.get())[i];
if ((entries_.get())[i].GetKey() != key)
{
auto key = entry.GetKey();
auto value = entry.GetValue();
Entry e(key, value);
(ts.entries_.get())[ts.num_entries_++] = e;
}
}
return ts;
}

// Returns true if there are no keys, false otherwise.
Expand All @@ -139,8 +286,81 @@ class TraceState
return nostd::span<Entry>(entries_.get(), num_entries_);
}

// Returns whether key is a valid key. See https://www.w3.org/TR/trace-context/#key
/** Returns whether key is a valid key. See https://www.w3.org/TR/trace-context/#key
* Identifiers MUST begin with a lowercase letter or a digit, and can only contain
* lowercase letters (a-z), digits (0-9), underscores (_), dashes (-), asterisks (*),
* and forward slashes (/).
* For multi-tenant vendor scenarios, an at sign (@) can be used to prefix the vendor name.
*
*/
static bool IsValidKey(nostd::string_view key)
{
#if HAVE_WORKING_REGEX
return IsValidKeyRegEx(key);
#else
return IsValidKeyNonRegEx(key);
#endif
}

/** Returns whether value is a valid value. See https://www.w3.org/TR/trace-context/#value
* The value is an opaque string containing up to 256 printable ASCII (RFC0020)
* characters ((i.e., the range 0x20 to 0x7E) except comma , and equal =)
*/
static bool IsValidValue(nostd::string_view value)
{
#if HAVE_WORKING_REGEX
return IsValidValueRegEx(value);
#else
return IsValidValueNonRegEx(value);
#endif
}

private:
// Store entries in a C-style array to avoid using std::array or std::vector.
nostd::unique_ptr<Entry[]> entries_;

// Maintain the number of entries in entries_. Must be in the range [0, kMaxKeyValuePairs].
size_t num_entries_;

// An empty TraceState.
TraceState() noexcept : entries_(new Entry[kMaxKeyValuePairs]), num_entries_(0) {}

static nostd::string_view TrimString(nostd::string_view str, size_t left, size_t right)
{
while (str[static_cast<std::size_t>(right)] == ' ' && left < right)
{
right--;
}
while (str[static_cast<std::size_t>(left)] == ' ' && left < right)
{
left++;
}
return str.substr(left, right - left + 1);
}

static bool IsValidKeyRegEx(nostd::string_view key)
{
static std::regex reg_key("^[a-z0-9][a-z0-9*_\\-/]{0,255}$");
static std::regex reg_key_multitenant(
"^[a-z0-9][a-z0-9*_\\-/]{0,240}(@)[a-z0-9][a-z0-9*_\\-/]{0,13}$");
std::string key_s(key);
if (std::regex_match(key_s, reg_key) || std::regex_match(key_s, reg_key_multitenant))
{
return true;
}
return false;
}

static bool IsValidValueRegEx(nostd::string_view value)
{
// Hex 0x20 to 0x2B, 0x2D to 0x3C, 0x3E to 0x7E
static std::regex reg_value(
"^[\\x20-\\x2B\\x2D-\\x3C\\x3E-\\x7E]{0,255}[\\x21-\\x2B\\x2D-\\x3C\\x3E-\\x7E]$");
// Need to benchmark without regex, as a string object is created here.
return std::regex_match(std::string(value), reg_value);
}

static bool IsValidKeyNonRegEx(nostd::string_view key)
{
if (key.empty() || key.size() > kKeyMaxSize || !IsLowerCaseAlphaOrDigit(key[0]))
{
Expand All @@ -163,8 +383,7 @@ class TraceState
return true;
}

// Returns whether value is a valid value. See https://www.w3.org/TR/trace-context/#value
static bool IsValidValue(nostd::string_view value)
static bool IsValidValueNonRegEx(nostd::string_view value)
{
if (value.empty() || value.size() > kValueMaxSize)
{
Expand All @@ -181,15 +400,7 @@ class TraceState
return true;
}

private:
// Store entries in a C-style array to avoid using std::array or std::vector.
nostd::unique_ptr<Entry[]> entries_;

// Maintain the number of entries in entries_. Must be in the range [0, kMaxKeyValuePairs].
int num_entries_;

static bool IsLowerCaseAlphaOrDigit(char c) { return isdigit(c) || islower(c); }
};

} // namespace trace
OPENTELEMETRY_END_NAMESPACE
OPENTELEMETRY_END_NAMESPACE
1 change: 1 addition & 0 deletions api/test/trace/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ foreach(
span_context_test
scope_test
noop_test
trace_state_test
tracer_test)
add_executable(api_${testname} "${testname}.cc")
target_link_libraries(
Expand Down
Loading

0 comments on commit ea87130

Please sign in to comment.