Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource sdk Implementation #502

Merged
merged 37 commits into from
Jan 22, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a27a5e4
resource api
lalitb Jan 5, 2021
abe8287
format and add tests
lalitb Jan 5, 2021
083b169
fix bazel
lalitb Jan 5, 2021
b61acdd
fix bazel another try
lalitb Jan 5, 2021
4540bc5
more fixes
lalitb Jan 5, 2021
dfe857c
fix resouce interface constructor
lalitb Jan 5, 2021
0ae679e
Merge branch 'master' into resources
lalitb Jan 5, 2021
7673007
fix review comments
lalitb Jan 9, 2021
d5b389f
revert change
lalitb Jan 11, 2021
9fbe8be
revert change
lalitb Jan 11, 2021
208f231
revert change
lalitb Jan 11, 2021
60034d9
fix merge conflict
lalitb Jan 12, 2021
04e9de8
Merge branch 'master' into resources
lalitb Jan 12, 2021
bfdd7e1
fix attributes_utils.h
lalitb Jan 12, 2021
2abd049
fix review comments
lalitb Jan 12, 2021
f71a47c
fix attributes
lalitb Jan 12, 2021
f7e4909
add doc
lalitb Jan 12, 2021
bc3bebc
fix static
lalitb Jan 12, 2021
48a30d4
fix test
lalitb Jan 12, 2021
d8d015a
review comments
lalitb Jan 17, 2021
48ab9ea
fix bazel build
lalitb Jan 17, 2021
f515165
remove un-necesaary include
lalitb Jan 17, 2021
5b01258
add test for different resoure types
lalitb Jan 18, 2021
e9745f1
fix otlp
lalitb Jan 18, 2021
84d5405
Merge branch 'master' into resources
lalitb Jan 18, 2021
01fd510
fix valgrind error
lalitb Jan 18, 2021
c60fefc
wMerge branch 'resources' of github.com:lalitb/opentelemetry-cpp into…
lalitb Jan 18, 2021
b023c2a
modify simple trace example to use resource
lalitb Jan 18, 2021
1586018
fix simple example
lalitb Jan 18, 2021
9158d71
make resource ctor private
lalitb Jan 21, 2021
db2412b
Merge branch 'master' into resources
lalitb Jan 21, 2021
35dbf99
fix access
lalitb Jan 21, 2021
5e67443
Merge branch 'resources' of github.com:lalitb/opentelemetry-cpp into …
lalitb Jan 21, 2021
adbd86d
fixes
lalitb Jan 21, 2021
ef45994
fix comment
lalitb Jan 21, 2021
a272b92
Update sdk/src/resource/resource.cc
lalitb Jan 22, 2021
8cd247e
Merge branch 'master' into resources
lalitb Jan 22, 2021
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
44 changes: 44 additions & 0 deletions sdk/include/opentelemetry/sdk/resource/resource.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

#include "opentelemetry/nostd/string_view.h"
#include "opentelemetry/nostd/unique_ptr.h"
#include "opentelemetry/sdk/trace/attribute_utils.h"
#include "opentelemetry/sdk/version/version.h"
#include "opentelemetry/version.h"

#include <cstdlib>
Copy link
Contributor

@ThomsonTan ThomsonTan Jan 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this include necessary?

Copy link
Member Author

@lalitb lalitb Jan 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we actually need it in resource.cpp ( to read env variable through std::getenv ). Will move it there. Thanks for noticing it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done now.

#include <memory>
#include <sstream>
#include <unordered_map>

OPENTELEMETRY_BEGIN_NAMESPACE
namespace sdk
{
namespace resource
{

using ResourceAttributes =
std::unordered_map<std::string, opentelemetry::sdk::trace::SpanDataAttributeValue>;

class Resource
{
public:
Resource(const ResourceAttributes &attributes = ResourceAttributes()) noexcept;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specification says that ANY created resource should go through the environment variable additions + default logic you have in Create. Should we hide this constructor to only the OTELResourceDetector + getDefault ?

Copy link
Member Author

@lalitb lalitb Jan 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is challenge to do this in C++. Ideally, I would like to make constructor as private, and make OTELResourceDetector as friend class of Resource, but we actually need all derivations of ResourceDetector have access to this constructor.
opentelementry-java does it using AutoValue annotation, and opentelemetry-dotnet does it using internal keyword.

Copy link
Member Author

@lalitb lalitb Jan 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Please ignore above comment. I have now made constructor protected ( so that it can be used in test cases ), and made OTELResourceDetector as its friend class. It seems we don't need to invoke Resource constructor in any other detector, so we should be fine.


const ResourceAttributes &GetAttributes() const noexcept;

std::shared_ptr<Resource> Merge(const Resource &other);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just return a stack-allocated resource here? Wouldn't that be a simpler api?

Similar for Create.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is possible to share resources across multiple spans, it would be better to use shared_ptr and avoid un-necessary copying.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method doesn't need to determine how resources are shared.

A few points:

I think it's reasonable for this method to return on the Stack. We then have our MetricProvider + TraceProvider have a Resource attribute they own, and Trace/Metrics generated from those provides can use Pointers or references to the resource associated with a provider.

Resources should really only be a startup concern and not something we expect users to pass around. I'd rather see a Resource&& argument on *Provider classes just to make ownership clear. This is similar to other APIs.

References:

Copy link
Member Author

@lalitb lalitb Jan 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree here. Resource need to be associated with TracerProvider at sdk level, and we can actually pass the reference of same to traces generated from the provider. And in that case, we don't need to manage smart pointers. Let me add other half in this PR, and it would be easier to comprehend.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, Resources::Merge() and Resources::Create() are modified to return stack allocated Resource by value. And this resource is then owned by sdk::tracer_provider and passed as reference to span. Please refer to examples/simple/main.cc in this PR on how the usage would be, and let me know if any issues now.


static std::shared_ptr<Resource> Create(const ResourceAttributes &attributes);

static Resource &GetEmpty();

static Resource &GetDefault();

private:
ResourceAttributes attributes_;
};

} // namespace resource
} // namespace sdk
OPENTELEMETRY_END_NAMESPACE
27 changes: 27 additions & 0 deletions sdk/include/opentelemetry/sdk/resource/resource_detector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once

#include "opentelemetry/nostd/unique_ptr.h"
#include "opentelemetry/sdk/resource/resource.h"
#include "opentelemetry/version.h"

OPENTELEMETRY_BEGIN_NAMESPACE
namespace sdk
{
namespace resource
{

class ResourceDetector
{
public:
virtual std::shared_ptr<opentelemetry::sdk::resource::Resource> Detect() = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same nit: Is there a reason to return a shared_ptr here? AFAIK Resource detection happens once and generally isn't passing pointers between different memory owners.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in earlier comment - As it is possible to share resources across multiple spans, it would be better to use shared_ptr and avoid un-necessary copying.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to avoid the counting of shared_ptr references if we can just use the attached TraceProvider.

I.e. shared_ptr has a cost, and I think we don't need it in this scenario. If we still want to use shared_ptr within the SDK to associate resource from *Provider to Span, that's fine but a different PR.

I think without seeing the other half of the change it's hard to fully comment on merits of shared_ptr.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I did revisit again, and probably we mayn't need smart pointer here ( including unique_ptr). Let me add other half of implementation in this PR, so that it would be easy to comprehend and review.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes are done as advised ( refer above comments ).

};

class OTELResourceDetector : public ResourceDetector
{
public:
std::shared_ptr<opentelemetry::sdk::resource::Resource> Detect() noexcept override;
};

} // namespace resource
} // namespace sdk
OPENTELEMETRY_END_NAMESPACE
13 changes: 13 additions & 0 deletions sdk/include/opentelemetry/sdk/trace/attribute_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ class AttributeMap
return attributes_;
}

void AddAttributes(const AttributeMap &other)
{
for (auto &attr : other.attributes_)
{
if ((attributes_.find(attr.first) == attributes_.end()) ||
(nostd::holds_alternative<std::string>(attributes_[attr.first]) &&
nostd::get<std::string>(attributes_[attr.first]).size() == 0))
{
attributes_[attr.first] = attr.second;
}
}
}

void SetAttribute(nostd::string_view key,
const opentelemetry::common::AttributeValue &value) noexcept
{
Expand Down
1 change: 1 addition & 0 deletions sdk/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ add_subdirectory(trace)
add_subdirectory(metrics)
add_subdirectory(logs)
add_subdirectory(version)
add_subdirectory(resource)
26 changes: 26 additions & 0 deletions sdk/src/resource/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2020, OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

package(default_visibility = ["//visibility:public"])

cc_library(
name = "resource",
srcs = glob(["**/*.cc"]),
hdrs = glob(["**/*.h"]),
include_prefix = "src/resource",
deps = [
"//api",
"//sdk:headers",
],
)
16 changes: 16 additions & 0 deletions sdk/src/resource/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
add_library(opentelemetry_resources resource.cc resource_detector.cc)

set_target_properties(opentelemetry_resources PROPERTIES EXPORT_NAME resources)

target_link_libraries(opentelemetry_resources opentelemetry_common)

target_include_directories(
opentelemetry_resources
PUBLIC "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/sdk/include>")

install(
TARGETS opentelemetry_resources
EXPORT "${PROJECT_NAME}-target"
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
67 changes: 67 additions & 0 deletions sdk/src/resource/resource.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include "opentelemetry/sdk/resource/resource.h"
#include "opentelemetry/nostd/span.h"
#include "opentelemetry/sdk/resource/resource_detector.h"
#include "opentelemetry/version.h"

OPENTELEMETRY_BEGIN_NAMESPACE
namespace sdk
{
namespace resource
{

const std::string TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name the constants like kTelemetrySdkLanguage?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. fixed now :)

const std::string TELEMETRY_SDK_NAME = "telemetry.sdk.name";
const std::string TELEMETRY_SDK_VERSION = "telemetry.sdk.version";

Resource::Resource(const ResourceAttributes &attributes) noexcept : attributes_(attributes) {}

std::shared_ptr<Resource> Resource::Merge(const Resource &other)
{
ResourceAttributes merged_resource_attributes(attributes_);
for (auto &elem : other.attributes_)
{
if ((merged_resource_attributes.find(elem.first) == merged_resource_attributes.end()) ||
(nostd::holds_alternative<std::string>(attributes_[elem.first]) &&
nostd::get<std::string>(attributes_[elem.first]).size() == 0))
{
merged_resource_attributes[elem.first] = elem.second;
}
}
return std::make_shared<Resource>(Resource(merged_resource_attributes));
}

std::shared_ptr<Resource> Resource::Create(const ResourceAttributes &attributes)
{
auto default_resource = Resource::GetDefault();

if (attributes.size() > 0)
{
Resource tmp_resource(attributes);
auto merged_resource = tmp_resource.Merge(default_resource);
return merged_resource->Merge(*(OTELResourceDetector().Detect()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Would it make sense to have both the OTEL resource + default resource be statics within this function?

{
  static auto default_resource = Resource::GetDefault();
  static auto otel_env_resource = OTELResourceDetector().Detect();
  ...
}

This would avoid re-running the OTEL resource detection for every created resource.
  

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I have changed it accordingly.

  static auto otel_resource = OTELResourceDetector().Detect();
  auto default_resource     = Resource::GetDefault();

and

Resource &Resource::GetDefault()
{
  static Resource default_resource({{TELEMETRY_SDK_LANGUAGE, "cpp"},
                                    {TELEMETRY_SDK_NAME, "opentelemetry"},
                                    {TELEMETRY_SDK_VERSION, OPENTELEMETRY_SDK_VERSION}});
  return default_resource;
}

}
return default_resource.Merge(*(OTELResourceDetector().Detect()));
}

Resource &Resource::GetEmpty()
{
static Resource empty;
return empty;
}

Resource &Resource::GetDefault()
{
static Resource default_resource({{TELEMETRY_SDK_LANGUAGE, "cpp"},
{TELEMETRY_SDK_NAME, "opentelemetry"},
{TELEMETRY_SDK_VERSION, OPENTELEMETRY_SDK_VERSION}});
return default_resource;
}

const ResourceAttributes &Resource::GetAttributes() const noexcept
{
return attributes_;
}

} // namespace resource
} // namespace sdk
OPENTELEMETRY_END_NAMESPACE
34 changes: 34 additions & 0 deletions sdk/src/resource/resource_detector.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include "opentelemetry/sdk/resource/resource_detector.h"
#include <cstdlib>

OPENTELEMETRY_BEGIN_NAMESPACE
namespace sdk
{
namespace resource
{

const char *OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES";

std::shared_ptr<Resource> OTELResourceDetector::Detect() noexcept
{
char *attributes_str = std::getenv(OTEL_RESOURCE_ATTRIBUTES);
if (attributes_str == nullptr)
return std::make_shared<Resource>(Resource::GetEmpty());

ResourceAttributes attributes;
std::istringstream iss(attributes_str);
std::string token;
while (std::getline(iss, token, ','))
{
size_t pos = token.find('=');
std::string key = token.substr(0, pos);
std::string value = token.substr(pos + 1);
attributes[key] = value;
}

return std::make_shared<Resource>(Resource(attributes));
}

} // namespace resource
} // namespace sdk
OPENTELEMETRY_END_NAMESPACE
1 change: 1 addition & 0 deletions sdk/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ add_subdirectory(common)
add_subdirectory(trace)
add_subdirectory(metrics)
add_subdirectory(logs)
add_subdirectory(resource)
11 changes: 11 additions & 0 deletions sdk/test/resource/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cc_test(
name = "resource_test",
srcs = [
"resource_test.cc",
],
deps = [
"//api",
"//sdk/src/resource",
"@com_google_googletest//:gtest_main",
],
)
9 changes: 9 additions & 0 deletions sdk/test/resource/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
foreach(testname resource_test)
add_executable(${testname} "${testname}.cc")
target_link_libraries(${testname} ${GTEST_BOTH_LIBRARIES}
${CMAKE_THREAD_LIBS_INIT} opentelemetry_resources)
gtest_add_tests(
TARGET ${testname}
TEST_PREFIX resources.
TEST_LIST ${testname})
endforeach()
122 changes: 122 additions & 0 deletions sdk/test/resource/resource_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#include "opentelemetry/sdk/resource/resource.h"
#include "opentelemetry/common/key_value_iterable_view.h"
#include "opentelemetry/nostd/string_view.h"
#include "opentelemetry/sdk/resource/resource_detector.h"
#include "opentelemetry/sdk/trace/attribute_utils.h"

#include <cstdlib>
#include <string>
#include <unordered_map>

#include <gtest/gtest.h>

TEST(ResourceTest, create)
{

std::map<std::string, std::string> expected_attributes = {
Copy link
Contributor

@maxgolov maxgolov Jan 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you supplement this with a test that handles strongly-typed attributes of numeric (integer) type? i.e. map of variants. Also - the cost parameter here should be a double, not string. Somebody who is onboarding to SDK should be able to freely express strongly-typed parameters in general. Just like it's done in nlohmann/json, where you'd provide developers with initializer list constructor, that is capable of accepting arbitrary variant types described within the OpenTelemetry spec.. Maybe not going as far as implementing the array types. But at least all primitive types in spec + std::string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sure that would be good test to showcase. I will add it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have modified the test to include both integer and double resource attributes. Please see if this is fine.

{"service", "backend"},
{"version", "1"},
{"cost", "234.23"},
{"telemetry.sdk.language", "cpp"},
{"telemetry.sdk.name", "opentelemetry"},
{"telemetry.sdk.version", OPENTELEMETRY_SDK_VERSION}};
auto resource = opentelemetry::sdk::resource::Resource::Create(
{{"service", "backend"}, {"version", "1"}, {"cost", "234.23"}});
auto received_attributes = resource->GetAttributes();

for (auto &e : received_attributes)
{
EXPECT_EQ(expected_attributes[e.first], opentelemetry::nostd::get<std::string>(e.second));
}
EXPECT_EQ(received_attributes.size(), expected_attributes.size());

opentelemetry::sdk::resource::ResourceAttributes attributes = {
{"service", "backend"}, {"version", "1"}, {"cost", "234.23"}};
auto resource2 = opentelemetry::sdk::resource::Resource::Create(attributes);
auto received_attributes2 = resource2->GetAttributes();
for (auto &e : received_attributes2)
{
EXPECT_TRUE(expected_attributes.find(e.first) != expected_attributes.end());
if (expected_attributes.find(e.first) != expected_attributes.end())
EXPECT_EQ(expected_attributes.find(e.first)->second,
opentelemetry::nostd::get<std::string>(e.second));
}
EXPECT_EQ(received_attributes2.size(), expected_attributes.size());
}

TEST(ResourceTest, Merge)
{
std::map<std::string, std::string> expected_attributes = {{"service", "backend"},
{"host", "service-host"}};
opentelemetry::sdk::resource::Resource resource1(
opentelemetry::sdk::resource::ResourceAttributes({{"service", "backend"}}));
opentelemetry::sdk::resource::Resource resource2(
opentelemetry::sdk::resource::ResourceAttributes({{"host", "service-host"}}));

auto merged_resource = resource1.Merge(resource2);
auto received_attributes = merged_resource->GetAttributes();
for (auto &e : received_attributes)
{
EXPECT_TRUE(expected_attributes.find(e.first) != expected_attributes.end());
if (expected_attributes.find(e.first) != expected_attributes.end())
EXPECT_EQ(expected_attributes.find(e.first)->second,
opentelemetry::nostd::get<std::string>(e.second));
}
EXPECT_EQ(received_attributes.size(), expected_attributes.size());
}

TEST(ResourceTest, MergeEmptyString)
{
std::map<std::string, std::string> expected_attributes = {{"service", "backend"},
{"host", "service-host"}};
opentelemetry::sdk::resource::Resource resource1({{"service", ""}, {"host", "service-host"}});
opentelemetry::sdk::resource::Resource resource2(
{{"service", "backend"}, {"host", "another-service-host"}});

auto merged_resource = resource1.Merge(resource2);
auto received_attributes = merged_resource->GetAttributes();
}

// this test uses putenv to set the env variable - this is not available on windows
#ifdef __linux__
TEST(ResourceTest, OtelResourceDetector)
{
std::map<std::string, std::string> expected_attributes = {{"k", "v"}};

char env[] = "OTEL_RESOURCE_ATTRIBUTES=k=v";
putenv(env);

opentelemetry::sdk::resource::OTELResourceDetector detector;
auto resource = detector.Detect();
auto received_attributes = resource->GetAttributes();
for (auto &e : received_attributes)
{
EXPECT_TRUE(expected_attributes.find(e.first) != expected_attributes.end());
if (expected_attributes.find(e.first) != expected_attributes.end())
EXPECT_EQ(expected_attributes.find(e.first)->second,
opentelemetry::nostd::get<std::string>(e.second));
}
EXPECT_EQ(received_attributes.size(), expected_attributes.size());
char env2[] = "OTEL_RESOURCE_ATTRIBUTES=";
putenv(env2);
}

TEST(ResourceTest, OtelResourceDetectorEmptyEnv)
{
std::map<std::string, std::string> expected_attributes = {};
char env[] = "OTEL_RESOURCE_ATTRIBUTES=";
putenv(env);
opentelemetry::sdk::resource::OTELResourceDetector detector;
auto resource = detector.Detect();
auto received_attributes = resource->GetAttributes();
for (auto &e : received_attributes)
{
EXPECT_TRUE(expected_attributes.find(e.first) != expected_attributes.end());
if (expected_attributes.find(e.first) != expected_attributes.end())
EXPECT_EQ(expected_attributes.find(e.first)->second,
opentelemetry::nostd::get<std::string>(e.second));
}
EXPECT_EQ(received_attributes.size(), expected_attributes.size());
}

#endif