Skip to content

Commit e0e0458

Browse files
authored
[SDK] Enforce Span Status transition rules (#4547)
1 parent e81d957 commit e0e0458

14 files changed

Lines changed: 372 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,18 @@ Increment the:
325325
metric exporter
326326
[#4365](https://github.com/open-telemetry/opentelemetry-cpp/pull/4365)
327327

328+
* [SDK] Enforce span status transition rules defined in the OpenTelemetry
329+
specification in `Span::SetStatus`.
330+
[#4546](https://github.com/open-telemetry/opentelemetry-cpp/issues/4546)
331+
* Previously every `SetStatus` call overwrote the recorded status. It now
332+
follows the specification, which changes behavior for users in three ways:
333+
* A call with `StatusCode::kUnset` is ignored.
334+
* Once the status is `kOk`, it is final. So a later call can no longer
335+
change an explicit `kOk`.
336+
* The description is kept only for `kError`; it is dropped for `kOk` and
337+
`kUnset`.
338+
* `ETWSpan::SetStatus` in the ETW exporter follows the same rules.
339+
328340
Important changes:
329341

330342
* [API] Never set a null global provider or propagator

‎exporters/etw/include/opentelemetry/exporters/etw/etw_tracer.h‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "opentelemetry/sdk/common/empty_attributes.h"
3636
#include "opentelemetry/sdk/trace/exporter.h"
3737
#include "opentelemetry/sdk/trace/samplers/always_on.h"
38+
#include "opentelemetry/sdk/trace/span_status.h"
3839

3940
#include "opentelemetry/exporters/etw/etw_config.h"
4041
#include "opentelemetry/exporters/etw/etw_fields.h"
@@ -952,8 +953,15 @@ class Span : public opentelemetry::trace::Span
952953
void SetStatus(opentelemetry::trace::StatusCode code,
953954
nostd::string_view description) noexcept override
954955
{
955-
status_code_ = code;
956-
status_description_ = description.data();
956+
const auto transition =
957+
sdk::trace::detail::ApplyStatusTransition(status_code_, code, description);
958+
if (!transition.accepted)
959+
{
960+
return;
961+
}
962+
963+
status_code_ = transition.code;
964+
status_description_.assign(transition.description.data(), transition.description.size());
957965
}
958966

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

‎exporters/etw/test/etw_tracer_test.cc‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#ifdef _WIN32
55

66
# include <gtest/gtest.h>
7+
# include <cstdint>
78
# include <map>
89
# include <string>
910
# include "opentelemetry//sdk/trace/sampler.h"
@@ -580,6 +581,86 @@ TEST(ETWTracer, EndWithCustomTime)
580581

581582
}
582583

584+
/**
585+
* Reads back the status fields the ETW exporter puts on the "Span" event at End.
586+
*/
587+
static void GetEmittedStatus(opentelemetry::trace::Span &span, uint32_t &code, std::string &message)
588+
{
589+
exporter::etw::Properties evt;
590+
static_cast<exporter::etw::Span &>(span).UpdateStatus(evt);
591+
const exporter::etw::PropertyVariant &code_value = evt[ETW_FIELD_STATUSCODE];
592+
const exporter::etw::PropertyVariant &message_value = evt[ETW_FIELD_STATUSMESSAGE];
593+
code = nostd::get<uint32_t>(code_value);
594+
message = nostd::get<std::string>(message_value);
595+
}
596+
597+
TEST(ETWTracer, SpanSetStatusOkIsFinal)
598+
{
599+
exporter::etw::TracerProvider tp;
600+
auto tracer = tp.GetTracer("SpanSetStatus");
601+
auto span = tracer->StartSpan("span 1");
602+
span->SetStatus(opentelemetry::trace::StatusCode::kOk, "");
603+
span->SetStatus(opentelemetry::trace::StatusCode::kError, "later error");
604+
span->SetStatus(opentelemetry::trace::StatusCode::kUnset, "");
605+
606+
uint32_t code = 0;
607+
std::string message;
608+
GetEmittedStatus(*span, code, message);
609+
EXPECT_EQ(code, static_cast<uint32_t>(opentelemetry::trace::StatusCode::kOk));
610+
EXPECT_EQ(message, "");
611+
span->End();
612+
}
613+
614+
TEST(ETWTracer, SpanSetStatusUnsetIsIgnored)
615+
{
616+
exporter::etw::TracerProvider tp;
617+
auto tracer = tp.GetTracer("SpanSetStatus");
618+
auto span = tracer->StartSpan("span 1");
619+
span->SetStatus(opentelemetry::trace::StatusCode::kError, "the real failure");
620+
span->SetStatus(opentelemetry::trace::StatusCode::kUnset, "");
621+
622+
uint32_t code = 0;
623+
std::string message;
624+
GetEmittedStatus(*span, code, message);
625+
EXPECT_EQ(code, static_cast<uint32_t>(opentelemetry::trace::StatusCode::kError));
626+
EXPECT_EQ(message, "the real failure");
627+
span->End();
628+
}
629+
630+
TEST(ETWTracer, SpanSetStatusDescriptionIgnoredForOk)
631+
{
632+
exporter::etw::TracerProvider tp;
633+
auto tracer = tp.GetTracer("SpanSetStatus");
634+
auto span = tracer->StartSpan("span 1");
635+
span->SetStatus(opentelemetry::trace::StatusCode::kError, "transient");
636+
span->SetStatus(opentelemetry::trace::StatusCode::kOk, "ignored description");
637+
638+
uint32_t code = 0;
639+
std::string message;
640+
GetEmittedStatus(*span, code, message);
641+
EXPECT_EQ(code, static_cast<uint32_t>(opentelemetry::trace::StatusCode::kOk));
642+
EXPECT_EQ(message, "");
643+
span->End();
644+
}
645+
646+
TEST(ETWTracer, SpanSetStatusErrorKeepsDescription)
647+
{
648+
exporter::etw::TracerProvider tp;
649+
auto tracer = tp.GetTracer("SpanSetStatus");
650+
auto span = tracer->StartSpan("span 1");
651+
// The description is not null terminated at its end, so a copy through data() alone overruns it.
652+
std::string description = "boomX";
653+
span->SetStatus(opentelemetry::trace::StatusCode::kError,
654+
nostd::string_view(description.data(), 4));
655+
656+
uint32_t code = 0;
657+
std::string message;
658+
GetEmittedStatus(*span, code, message);
659+
EXPECT_EQ(code, static_cast<uint32_t>(opentelemetry::trace::StatusCode::kError));
660+
EXPECT_EQ(message, "boom");
661+
span->End();
662+
}
663+
583664
TEST(ETWTracer, ConstructorInitializesToOpenState)
584665
{
585666
opentelemetry::exporter::etw::TracerProvider provider;

‎exporters/otlp/src/otlp_recordable.cc‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ void OtlpRecordable::SetStatus(trace::StatusCode code, nostd::string_view descri
213213
{
214214
span_.mutable_status()->set_message(description.data(), description.size());
215215
}
216+
else
217+
{
218+
span_.mutable_status()->clear_message();
219+
}
216220
}
217221

218222
void OtlpRecordable::SetName(nostd::string_view name) noexcept

‎exporters/otlp/test/otlp_recordable_test.cc‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,18 @@ TEST(OtlpRecordable, SetStatus)
190190
EXPECT_EQ(rec2.span().status().message(), "");
191191
}
192192

193+
TEST(OtlpRecordable, SetStatusClearsStaleMessage)
194+
{
195+
OtlpRecordable rec;
196+
rec.SetStatus(trace::StatusCode::kError, "boom");
197+
EXPECT_EQ(rec.span().status().message(), "boom");
198+
199+
rec.SetStatus(trace::StatusCode::kOk, "");
200+
EXPECT_EQ(rec.span().status().code(),
201+
proto::trace::v1::Status_StatusCode(trace::StatusCode::kOk));
202+
EXPECT_EQ(rec.span().status().message(), "");
203+
}
204+
193205
TEST(OtlpRecordable, SetTraceFlags)
194206
{
195207
OtlpRecordable rec;

‎exporters/zipkin/src/recordable.cc‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ void Recordable::SetStatus(trace::StatusCode code, nostd::string_view descriptio
215215
{
216216
span_["tags"]["error"] = description;
217217
}
218+
else
219+
{
220+
span_["tags"].erase("error");
221+
}
218222
}
219223
}
220224

‎exporters/zipkin/test/zipkin_recordable_test.cc‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,19 @@ TEST(ZipkinSpanRecordable, SetStatus)
156156
}
157157
}
158158

159+
TEST(ZipkinSpanRecordable, SetStatusClearsStaleErrorTag)
160+
{
161+
zipkin::Recordable rec;
162+
163+
rec.SetStatus(trace::StatusCode::kError, "boom");
164+
EXPECT_EQ(rec.span()["tags"]["error"], "boom");
165+
166+
rec.SetStatus(trace::StatusCode::kOk, "");
167+
const json j_span = {{"tags", {{"otel.status_code", trace::StatusCode::kOk}}}};
168+
EXPECT_EQ(rec.span(), j_span);
169+
EXPECT_FALSE(rec.span()["tags"].contains("error"));
170+
}
171+
159172
TEST(ZipkinSpanRecordable, SetSpanKind)
160173
{
161174
json j_json_client = {{"kind", "CLIENT"}};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#pragma once
5+
6+
#include "opentelemetry/nostd/string_view.h"
7+
#include "opentelemetry/trace/span_metadata.h"
8+
#include "opentelemetry/version.h"
9+
10+
OPENTELEMETRY_BEGIN_NAMESPACE
11+
namespace sdk
12+
{
13+
namespace trace
14+
{
15+
namespace detail
16+
{
17+
18+
/**
19+
* Outcome of applying the span status transition rules to a SetStatus call.
20+
*/
21+
struct StatusTransition
22+
{
23+
/// Whether the call changes the status. When false, the SetStatus call must be ignored.
24+
bool accepted{false};
25+
26+
/// Status code to record.
27+
opentelemetry::trace::StatusCode code{opentelemetry::trace::StatusCode::kUnset};
28+
29+
/// Description to record.
30+
nostd::string_view description{""};
31+
};
32+
33+
/**
34+
* @brief Applies the span status transition rules from the trace API specification.
35+
*
36+
* @param current Status code recorded so far, kUnset if SetStatus was never called.
37+
* @param code Status code passed to SetStatus.
38+
* @param description Description passed to SetStatus.
39+
*/
40+
inline StatusTransition ApplyStatusTransition(opentelemetry::trace::StatusCode current,
41+
opentelemetry::trace::StatusCode code,
42+
nostd::string_view description) noexcept
43+
{
44+
const auto is_already_ok{current == opentelemetry::trace::StatusCode::kOk};
45+
const auto is_unset{code == opentelemetry::trace::StatusCode::kUnset};
46+
47+
if (is_already_ok || is_unset)
48+
{
49+
return StatusTransition{false, current, nostd::string_view{""}};
50+
}
51+
52+
const auto keep_description{(code == opentelemetry::trace::StatusCode::kError) &&
53+
!description.empty()};
54+
const auto effective_description{keep_description ? description : nostd::string_view{""}};
55+
56+
return StatusTransition{true, code, effective_description};
57+
}
58+
59+
} // namespace detail
60+
} // namespace trace
61+
} // namespace sdk
62+
OPENTELEMETRY_END_NAMESPACE

‎sdk/src/trace/span.cc‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "opentelemetry/nostd/function_ref.h"
99
#include "opentelemetry/sdk/trace/processor.h"
1010
#include "opentelemetry/sdk/trace/recordable.h"
11+
#include "opentelemetry/sdk/trace/span_status.h"
1112
#include "opentelemetry/trace/span_id.h"
1213
#include "opentelemetry/trace/span_metadata.h"
1314
#include "opentelemetry/trace/trace_flags.h"
@@ -201,7 +202,15 @@ void Span::SetStatus(opentelemetry::trace::StatusCode code, nostd::string_view d
201202
{
202203
return;
203204
}
204-
recordable_->SetStatus(code, description);
205+
206+
const auto transition = detail::ApplyStatusTransition(status_code_, code, description);
207+
if (!transition.accepted)
208+
{
209+
return;
210+
}
211+
212+
status_code_ = transition.code;
213+
recordable_->SetStatus(transition.code, transition.description);
205214
}
206215

207216
void Span::UpdateName(nostd::string_view name) noexcept

‎sdk/src/trace/span.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "opentelemetry/trace/span.h"
1717
#include "opentelemetry/trace/span_context.h"
1818
#include "opentelemetry/trace/span_context_kv_iterable.h"
19+
#include "opentelemetry/trace/span_metadata.h"
1920
#include "opentelemetry/trace/span_startoptions.h"
2021
#include "opentelemetry/version.h"
2122

@@ -83,6 +84,7 @@ class Span final : public opentelemetry::trace::Span
8384
std::unique_ptr<Recordable> recordable_;
8485
opentelemetry::common::SteadyTimestamp start_steady_time;
8586
opentelemetry::trace::SpanContext span_context_;
87+
opentelemetry::trace::StatusCode status_code_{opentelemetry::trace::StatusCode::kUnset};
8688
bool has_ended_{false};
8789
};
8890
} // namespace trace

0 commit comments

Comments
 (0)