-
-
Notifications
You must be signed in to change notification settings - Fork 199
feat: Implement distributed trace propagation (NATIVE-304) #657
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -121,6 +121,47 @@ sentry_transaction_context_remove_sampled(sentry_transaction_context_t *tx_cxt) | |
| sentry_value_remove_by_key(tx_cxt->inner, "sampled"); | ||
| } | ||
|
|
||
| void | ||
| sentry_transaction_context_update_from_header( | ||
| sentry_transaction_context_t *tx_cxt, const char *key, const char *value) | ||
| { | ||
| if (!sentry__string_eq(key, "sentry-trace")) { | ||
| return; | ||
| } | ||
|
|
||
| // https://develop.sentry.dev/sdk/performance/#header-sentry-trace | ||
| // sentry-trace = traceid-spanid(-sampled)? | ||
| const char *trace_id_start = value; | ||
| const char *trace_id_end = strchr(trace_id_start, '-'); | ||
| if (!trace_id_end) { | ||
| return; | ||
| } | ||
|
|
||
| sentry_value_t inner = tx_cxt->inner; | ||
|
|
||
| char *s | ||
| = sentry__string_clonen(trace_id_start, trace_id_end - trace_id_start); | ||
| sentry_value_t trace_id = sentry__value_new_string_owned(s); | ||
| sentry_value_set_by_key(inner, "trace_id", trace_id); | ||
|
|
||
| const char *span_id_start = trace_id_end + 1; | ||
| const char *span_id_end = strchr(span_id_start, '-'); | ||
| if (!span_id_end) { | ||
| // no sampled flag | ||
| sentry_value_t parent_span_id = sentry_value_new_string(span_id_start); | ||
| sentry_value_set_by_key(inner, "parent_span_id", parent_span_id); | ||
| return; | ||
| } | ||
| // else: we have a sampled flag | ||
|
|
||
| s = sentry__string_clonen(span_id_start, span_id_end - span_id_start); | ||
| sentry_value_t parent_span_id = sentry__value_new_string_owned(s); | ||
| sentry_value_set_by_key(inner, "parent_span_id", parent_span_id); | ||
|
|
||
| bool sampled = *(span_id_end + 1) == '1'; | ||
| sentry_value_set_by_key(inner, "sampled", sentry_value_new_bool(sampled)); | ||
| } | ||
|
|
||
| sentry_transaction_t * | ||
| sentry__transaction_new(sentry_value_t inner) | ||
| { | ||
|
|
@@ -431,3 +472,37 @@ sentry_transaction_set_status( | |
| { | ||
| set_status(tx->inner, status); | ||
| } | ||
|
|
||
| static void | ||
| sentry__span_iter_headers(sentry_value_t span, | ||
| sentry_iter_headers_function_t callback, void *userdata) | ||
| { | ||
| sentry_value_t trace_id = sentry_value_get_by_key(span, "trace_id"); | ||
| sentry_value_t span_id = sentry_value_get_by_key(span, "span_id"); | ||
| sentry_value_t sampled = sentry_value_get_by_key(span, "sampled"); | ||
|
|
||
| if (sentry_value_is_null(trace_id) || sentry_value_is_null(span_id)) { | ||
| return; | ||
| } | ||
|
|
||
| char buf[64]; | ||
| snprintf(buf, sizeof(buf), "%s-%s-%s", sentry_value_as_string(trace_id), | ||
| sentry_value_as_string(span_id), | ||
| sentry_value_is_true(sampled) ? "1" : "0"); | ||
|
Comment on lines
+488
to
+491
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this guaranteed not to be more that 64 chars? I mean really shouldn't the return code of snprintf be checked?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pretty much, yes. |
||
|
|
||
| callback("sentry-trace", buf, userdata); | ||
| } | ||
|
|
||
| void | ||
| sentry_span_iter_headers(sentry_span_t *span, | ||
| sentry_iter_headers_function_t callback, void *userdata) | ||
| { | ||
| sentry__span_iter_headers(span->inner, callback, userdata); | ||
| } | ||
|
|
||
| void | ||
| sentry_transaction_iter_headers(sentry_transaction_t *tx, | ||
| sentry_iter_headers_function_t callback, void *userdata) | ||
| { | ||
| sentry__span_iter_headers(tx->inner, callback, userdata); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| #include "sentry_scope.h" | ||
| #include "sentry_testsupport.h" | ||
|
|
||
| #include "sentry_scope.h" | ||
| #include "sentry_string.h" | ||
| #include "sentry_tracing.h" | ||
| #include "sentry_uuid.h" | ||
|
|
||
|
|
@@ -663,5 +665,99 @@ SENTRY_TEST(drop_unfinished_spans) | |
| TEST_CHECK_INT_EQUAL(called_transport, 1); | ||
| } | ||
|
|
||
| static void | ||
| forward_headers_to(const char *key, const char *value, void *userdata) | ||
| { | ||
| sentry_transaction_context_t *tx_ctx | ||
| = (sentry_transaction_context_t *)userdata; | ||
|
|
||
| sentry_transaction_context_update_from_header(tx_ctx, key, value); | ||
| } | ||
|
|
||
| SENTRY_TEST(distributed_headers) | ||
| { | ||
| sentry_options_t *options = sentry_options_new(); | ||
| sentry_options_set_dsn(options, "https://[email protected]/42"); | ||
|
|
||
| sentry_options_set_traces_sample_rate(options, 1.0); | ||
| sentry_options_set_max_spans(options, 2); | ||
| sentry_init(options); | ||
|
|
||
| sentry_transaction_context_t *tx_ctx | ||
| = sentry_transaction_context_new("wow!", NULL); | ||
| sentry_transaction_t *tx = sentry_transaction_start(tx_ctx); | ||
|
|
||
| const char *trace_id = sentry_value_as_string( | ||
| sentry_value_get_by_key(tx->inner, "trace_id")); | ||
| TEST_CHECK(!sentry__string_eq(trace_id, "")); | ||
|
|
||
| const char *span_id | ||
| = sentry_value_as_string(sentry_value_get_by_key(tx->inner, "span_id")); | ||
| TEST_CHECK(!sentry__string_eq(span_id, "")); | ||
|
|
||
| // check transaction | ||
| tx_ctx = sentry_transaction_context_new("distributed!", NULL); | ||
| sentry_transaction_iter_headers(tx, forward_headers_to, (void *)tx_ctx); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems like a fairly unconventional signature given what i understand of the intent of the api's design: the way i read it was that you would use the headers getter/setter like so, using this test as an example: this appears to have pushed aside the idea of returning some sort of map in favour of a more "iterable"-like api. is there a particular reason why that decision was made? does it make using this more natural over returning some sort of map? stepping outside of the scope of the test itself, the return value is presumably going to be attached to the headers of requests (i.e. events and transactions) sent to sentry, so would this api still feel natural once we add that functionality?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, maps don’t exist in C, neither do "standard" iterators.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the generic alternative would be to define a struct with key, value, next and force people to populate that. but really it's no better than this way i guess, passing the userdata around saves some conversions in favour of more function calls. |
||
| sentry_transaction_t *dist_tx = sentry_transaction_start(tx_ctx); | ||
|
|
||
| const char *dist_trace_id = sentry_value_as_string( | ||
| sentry_value_get_by_key(dist_tx->inner, "trace_id")); | ||
| TEST_CHECK_STRING_EQUAL(dist_trace_id, trace_id); | ||
|
|
||
| const char *parent_span_id = sentry_value_as_string( | ||
| sentry_value_get_by_key(dist_tx->inner, "parent_span_id")); | ||
| TEST_CHECK_STRING_EQUAL(parent_span_id, span_id); | ||
|
|
||
| sentry__transaction_decref(dist_tx); | ||
|
|
||
| // check span | ||
| sentry_span_t *child = sentry_transaction_start_child(tx, "honk", "goose"); | ||
|
|
||
| span_id = sentry_value_as_string( | ||
| sentry_value_get_by_key(child->inner, "span_id")); | ||
| TEST_CHECK(!sentry__string_eq(span_id, "")); | ||
|
|
||
| tx_ctx = sentry_transaction_context_new("distributed!", NULL); | ||
| sentry_span_iter_headers(child, forward_headers_to, (void *)tx_ctx); | ||
| dist_tx = sentry_transaction_start(tx_ctx); | ||
|
|
||
| dist_trace_id = sentry_value_as_string( | ||
| sentry_value_get_by_key(dist_tx->inner, "trace_id")); | ||
| TEST_CHECK_STRING_EQUAL(dist_trace_id, trace_id); | ||
|
|
||
| parent_span_id = sentry_value_as_string( | ||
| sentry_value_get_by_key(dist_tx->inner, "parent_span_id")); | ||
| TEST_CHECK_STRING_EQUAL(parent_span_id, span_id); | ||
|
|
||
| TEST_CHECK(sentry_value_is_true( | ||
| sentry_value_get_by_key(dist_tx->inner, "sampled"))); | ||
|
|
||
| sentry__transaction_decref(dist_tx); | ||
| sentry__span_free(child); | ||
| sentry__transaction_decref(tx); | ||
|
|
||
| // check sampled flag | ||
| tx_ctx = sentry_transaction_context_new("wow!", NULL); | ||
| sentry_transaction_context_set_sampled(tx_ctx, 0); | ||
| tx = sentry_transaction_start(tx_ctx); | ||
|
|
||
| tx_ctx = sentry_transaction_context_new("distributed!", NULL); | ||
| sentry_transaction_iter_headers(tx, forward_headers_to, (void *)tx_ctx); | ||
| dist_tx = sentry_transaction_start(tx_ctx); | ||
|
|
||
| TEST_CHECK(!sentry_value_is_true( | ||
| sentry_value_get_by_key(dist_tx->inner, "sampled"))); | ||
|
|
||
| sentry__transaction_decref(dist_tx); | ||
|
|
||
| // TODO: Check the sampled flag on a child span as well, but I think we | ||
| // don't create one if the transaction is not sampled? Well, here is the | ||
| // reason why we should! | ||
|
Comment on lines
+753
to
+755
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm a little confused as to how this would make for a good use case to carry forward the sampled field into a child span. if it's purely for testing purposes, i can sort of see the argument. it seems like we could just expose unit test-exclusive functionality to check this, though. if i were to consider the cases outside of the unit test, i'll admit that it's still not too clear to me how and where unsampled spans will be used. i do recognize the fact that other SDKs do still construct spans even if they're unsampled, and perhaps that's enough of an argument to do so. from the perspective of this feature (distributed trace propagation), a need to continue a trace from an unsampled span and not an unsampled transaction is what's required to make constructing unsampled spans useful. however, it's not clear to me what sort of situations would form the basis of that requirement. are users directly grabbing headers from a span? do we have instrumentation in this SDK that requires grabbing headers from spans?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://develop.sentry.dev/sdk/performance/#propagation
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure; the sampling decision does get implicitly propagated by not constructing spans if they're not sampled in the native SDK right now. the only scenario where the native SDK doesn't work is if we're continuing an unsampled span across service boundaries. we can continue unsampled transactions across service boundaries in the native SDK, and the one place where you would normally look for a transaction to do we need to continue unsampled spans? do you ever need to invoke
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. either way we could probably retroactively add this in if it's needed, ie it really doesn't affect the primary purpose of this PR (unless not having this straight up breaks distributed tracing but we'd fix that in a follow-up PR anyways)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
absolutely! You attach the headers (continue the trace) from whereever you currently are. |
||
|
|
||
| sentry__transaction_decref(tx); | ||
|
|
||
| sentry_close(); | ||
| } | ||
|
|
||
| #undef IS_NULL | ||
| #undef CHECK_STRING_PROPERTY | ||
Uh oh!
There was an error while loading. Please reload this page.