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

[ETW Exporter] Tail based sampling support #1780

Merged
merged 18 commits into from
Dec 2, 2022
Merged
10 changes: 10 additions & 0 deletions exporters/etw/include/opentelemetry/exporters/etw/etw_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include "opentelemetry//sdk/trace/sampler.h"
#include "opentelemetry/exporters/etw/etw_provider.h"
#include "opentelemetry/exporters/etw/etw_tail_sampler.h"
#include "opentelemetry/sdk/trace/id_generator.h"

OPENTELEMETRY_BEGIN_NAMESPACE
Expand Down Expand Up @@ -166,6 +167,15 @@ sdk::trace::Sampler &GetSampler(T &t)
return *t.sampler_;
}

/**
* @brief Utility function to obtain etw::TracerProvider.tail_sampler_
*/
template <class T>
TailSampler &GetTailSampler(T &t)
{
return *t.tail_sampler_;
}

/**
* @brief Utility template to convert SpanId or TraceId to hex.
* @param id - value of SpanId or TraceId
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
#include "opentelemetry/sdk/trace/sampler.h"
#include "opentelemetry/trace/span.h"

#pragma once
OPENTELEMETRY_BEGIN_NAMESPACE
namespace exporter
{
namespace etw
{

class TailSampler
{
public:
// convert to etw span if required for getters on span.
// auto etw_span = static_cast<const opentelemetry::exporter::etw::Span*>(&span);
// Decision based on
// Span::GetStatus()
// Span::GetProperties()
// Span::GetContext()
virtual opentelemetry::sdk::trace::SamplingResult ShouldSample(
const opentelemetry::trace::Span &span) noexcept = 0;
};

class AlwaysOnTailSampler : public TailSampler
{
public:
opentelemetry::sdk::trace::SamplingResult ShouldSample(
const opentelemetry::trace::Span &span) noexcept override
{
return {opentelemetry::sdk::trace::Decision::RECORD_AND_SAMPLE};
}
};

} // namespace etw
} // namespace exporter
OPENTELEMETRY_END_NAMESPACE
63 changes: 49 additions & 14 deletions exporters/etw/include/opentelemetry/exporters/etw/etw_tracer.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include "opentelemetry/exporters/etw/etw_properties.h"
#include "opentelemetry/exporters/etw/etw_provider.h"
#include "opentelemetry/exporters/etw/etw_random_id_generator.h"
#include "opentelemetry/exporters/etw/etw_tail_sampler.h"
#include "opentelemetry/exporters/etw/utils.h"

OPENTELEMETRY_BEGIN_NAMESPACE
Expand Down Expand Up @@ -235,9 +236,17 @@ class Tracer : public opentelemetry::trace::Tracer,
const opentelemetry::trace::Span *parentSpan = nullptr,
const opentelemetry::trace::EndSpanOptions & = {})
{
const auto &cfg = GetConfiguration(tracerProvider_);
const auto &cfg = GetConfiguration(tracerProvider_);
const auto &tail_sampler = GetTailSampler(tracerProvider_);
const opentelemetry::trace::Span &spanBase =
reinterpret_cast<const opentelemetry::trace::Span &>(span);

// Sample span based on the decision of tail based sampler
auto sampling_result = const_cast<TailSampler *>(&tail_sampler)->ShouldSample(spanBase);
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of making a virtual call to AlwaysOn sampler by default, perhaps check if there is a non-default sampler set, then make the virtual call? In this way, it doesn't introduce extra cost (except the null check) when tail based sampler is not used.

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 comment. Actually we do have virtual method calls for head-sampler, id-generator both in ETW exporter, and the otel-sdk. There is no reported performance reported with them. Seemingly modern compilers can optimize these repeated calls by caching the address, and avoid virtual indirection. Perhaps we can look back to this if there are reported overhead internally and/or by community.

if (!sampling_result.IsRecording() || !sampling_result.IsSampled())
{
return;
}
auto spanContext = spanBase.GetContext();

// Populate Span with presaved attributes
Expand Down Expand Up @@ -375,17 +384,23 @@ class Tracer : public opentelemetry::trace::Tracer,
const opentelemetry::trace::SpanContextKeyValueIterable &links,
const opentelemetry::trace::StartSpanOptions &options = {}) noexcept override
{
#ifdef OPENTELEMETRY_RTTI_ENABLED
// If RTTI is enabled by compiler, the below code modifies the attributes object passed as arg,
// which is sometime not desirable, set OPENTELEMETRY_NOT_USE_RTTI in application
// to avoid using RTTI in that case in below set of code.
#if !defined OPENTELEMETRY_RTTI_ENABLED || defined OPENTELEMETRY_NOT_USE_RTTI
Properties evtCopy = attributes;
return StartSpan(name, evtCopy, links, options);
#else // OPENTELEMETRY_RTTI_ENABLED is defined
common::KeyValueIterable &attribs = const_cast<common::KeyValueIterable &>(attributes);
Properties *evt = dynamic_cast<Properties *>(&attribs);
if (evt != nullptr)
{
// Pass as a reference to original modifyable collection without creating a copy
return StartSpan(name, *evt, links, options);
}
#endif
Properties evtCopy = attributes;
return StartSpan(name, evtCopy, links, options);
#endif
}

/**
Expand Down Expand Up @@ -557,18 +572,24 @@ class Tracer : public opentelemetry::trace::Tracer,
common::SystemTimestamp timestamp,
const common::KeyValueIterable &attributes) noexcept
{
#ifdef OPENTELEMETRY_RTTI_ENABLED
// If RTTI is enabled by compiler, the below code modifies the attributes object passed as arg,
// which is sometime not desirable, set OPENTELEMETRY_NOT_USE_RTTI in application
// to avoid using RTTI in that case in below set of code.
#if !defined OPENTELEMETRY_RTTI_ENABLED || defined OPENTELEMETRY_NOT_USE_RTTI
// Pass a copy converted to Properties object on stack
Properties evtCopy = attributes;
return AddEvent(span, name, timestamp, evtCopy);
#else // OPENTELEMETRY_RTTI_ENABLED is defined
common::KeyValueIterable &attribs = const_cast<common::KeyValueIterable &>(attributes);
Properties *evt = dynamic_cast<Properties *>(&attribs);
if (evt != nullptr)
{
// Pass as a reference to original modifyable collection without creating a copy
return AddEvent(span, name, timestamp, *evt);
}
#endif
// Pass a copy converted to Properties object on stack
Properties evtCopy = attributes;
return AddEvent(span, name, timestamp, evtCopy);
#endif
}

/**
Expand Down Expand Up @@ -826,6 +847,8 @@ class Span : public opentelemetry::trace::Span
status_description_ = description.data();
}

opentelemetry::trace::StatusCode GetStatus() { return status_code_; }

void SetAttributes(Properties attributes) { attributes_ = attributes; }

/**
Expand Down Expand Up @@ -932,6 +955,12 @@ class TracerProvider : public opentelemetry::trace::TracerProvider
*/
std::unique_ptr<sdk::trace::Sampler> sampler_;

/**
* @brief Sampler configured
*
*/
std::unique_ptr<opentelemetry::exporter::etw::TailSampler> tail_sampler_;

/**
* @brief IdGenerator for trace_id and span_id
*
Expand All @@ -942,15 +971,19 @@ class TracerProvider : public opentelemetry::trace::TracerProvider
* @brief Construct instance of TracerProvider with given options
* @param options Configuration options
*/
TracerProvider(TelemetryProviderOptions options,
std::unique_ptr<sdk::trace::Sampler> sampler =
std::unique_ptr<sdk::trace::AlwaysOnSampler>(new sdk::trace::AlwaysOnSampler),
std::unique_ptr<sdk::trace::IdGenerator> id_generator =
std::unique_ptr<opentelemetry::sdk::trace::IdGenerator>(
new sdk::trace::ETWRandomIdGenerator()))
TracerProvider(
TelemetryProviderOptions options,
std::unique_ptr<sdk::trace::Sampler> sampler =
std::unique_ptr<sdk::trace::AlwaysOnSampler>(new sdk::trace::AlwaysOnSampler),
std::unique_ptr<sdk::trace::IdGenerator> id_generator =
std::unique_ptr<opentelemetry::sdk::trace::IdGenerator>(
new sdk::trace::ETWRandomIdGenerator()),
std::unique_ptr<opentelemetry::exporter::etw::TailSampler> tail_sampler =
std::unique_ptr<opentelemetry::exporter::etw::TailSampler>(new AlwaysOnTailSampler()))
: opentelemetry::trace::TracerProvider(),
sampler_{std::move(sampler)},
id_generator_{std::move(id_generator)}
id_generator_{std::move(id_generator)},
tail_sampler_{std::move(tail_sampler)}
{
// By default we ensure that all events carry their with TraceId and SpanId
GetOption(options, "enableTraceId", config_.enableTraceId, true);
Expand Down Expand Up @@ -985,7 +1018,9 @@ class TracerProvider : public opentelemetry::trace::TracerProvider
: opentelemetry::trace::TracerProvider(),
sampler_{std::unique_ptr<sdk::trace::AlwaysOnSampler>(new sdk::trace::AlwaysOnSampler)},
id_generator_{std::unique_ptr<opentelemetry::sdk::trace::IdGenerator>(
new sdk::trace::ETWRandomIdGenerator())}
new sdk::trace::ETWRandomIdGenerator())},
tail_sampler_{
std::unique_ptr<opentelemetry::exporter::etw::TailSampler>(new AlwaysOnTailSampler())}
{
config_.enableTraceId = true;
config_.enableSpanId = true;
Expand Down
31 changes: 30 additions & 1 deletion exporters/etw/test/etw_tracer_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
# include <gtest/gtest.h>
# include <map>
# include <string>

# include "opentelemetry//sdk/trace/sampler.h"
# include "opentelemetry/exporters/etw/etw_tracer_exporter.h"
# include "opentelemetry/sdk/trace/samplers/always_off.h"
Expand Down Expand Up @@ -78,6 +77,16 @@ class MockSampler : public sdk::trace::Sampler
std::shared_ptr<Sampler> delegate_sampler_;
};

class AlwaysOffTailSampler : public TailSampler
{
public:
opentelemetry::sdk::trace::SamplingResult ShouldSample(
const opentelemetry::trace::Span &span) noexcept override
{
return {opentelemetry::sdk::trace::Decision::DROP};
}
};

/* clang-format off */
TEST(ETWTracer, TracerCheck)
{
Expand Down Expand Up @@ -459,6 +468,26 @@ TEST(ETWTracer, AlwayOffSampler)
EXPECT_EQ(span->GetContext().IsSampled(), false);
}

TEST(ETWTracer, AlwayOffTailSampler)
{
std::string providerName = kGlobalProviderName; // supply unique instrumentation name here
std::unique_ptr<sdk::trace::Sampler> always_on{new sdk::trace::AlwaysOnSampler()};
sdk::trace::IdGenerator *id_generator = new MockIdGenerator();
std::unique_ptr<TailSampler> always_off_tail{new AlwaysOffTailSampler()};
exporter::etw::TracerProvider tp
({
{"enableTraceId", true},
{"enableSpanId", true},
{"enableActivityId", true},
{"enableRelatedActivityId", true},
{"enableAutoParent", true}
},
std::move(always_on),
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we start a span and test it is indeed dropped?

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 was thinking to mock the actual ETW transport layer and do the test. In current design, this is not easy to do, perhaps can be taken separately if it is fine?

std::unique_ptr<sdk::trace::IdGenerator>(id_generator),
std::move(always_off_tail));
auto tracer = tp.GetTracer(providerName);
}

TEST(ETWTracer, CustomIdGenerator)
{
std::string providerName = kGlobalProviderName; // supply unique instrumentation name here
Expand Down