RUM-562: Report configured distributed tracing headers as part of Configuration telemetry#2572
Conversation
38c23db to
90b56fd
Compare
| testedCore.features += Feature.TRACING_FEATURE_NAME to mockTracingFeature | ||
|
|
||
| val traceSampleRate = forge.aFloat(min = 0f, max = 100f) | ||
| val hostsWithHeaderTypes: HostsWithHeaderTypes? = forge.aNullable<HostsWithHeaderTypes>() |
There was a problem hiding this comment.
| val hostsWithHeaderTypes: HostsWithHeaderTypes? = forge.aNullable<HostsWithHeaderTypes>() | |
| val hostsWithHeaderTypes = forge.aNullable<HostsWithHeaderTypes>() |
| override fun getForgery(forge: Forge): HostsWithHeaderTypes { | ||
| return HostsWithHeaderTypes( | ||
| hosts = forge.aMap { | ||
| aString() to aList { |
There was a problem hiding this comment.
minor: maybe generate something like a host look-like? by using say aStringMatching("https://[a-z0-9]+\\.com")
| return headerTypes.hosts | ||
| .values |
There was a problem hiding this comment.
so why do we collect (or in other words, have it in the model) hosts information if in the end we only send header types to the configuration telemetry?
There was a problem hiding this comment.
I felt that if we are making some information (header types) available to the outside it is better to expose "the original" information, not the result of some function which removes most of it.
Suppose we need something else connected to these hosts and headers (maybe a set of hosts?), now we can just use the thing I did here.
However I actually don't have a strong opinion here, one can argue for example that it is better to expose as little as possible to the outside.
I have no objections to refactor the code to expose only a set of header types.
There was a problem hiding this comment.
yes, let's change this PR to have only tracing header types passed around, it will greatly simply the things. We can add hosts information later if needed.
| import com.datadog.android.telemetry.model.TelemetryConfigurationEvent | ||
|
|
||
| internal object HostsWithHeaderTypesMapper { | ||
| fun mapToSelectedTracingPropagators( |
There was a problem hiding this comment.
suggestion: maybe have it as an extension method is simpler?
| val host1 = forge.aString() | ||
| val host2 = forge.aString() | ||
| val host3 = forge.aString() | ||
|
|
||
| val headerType1 = forge.aValueFrom(HostsWithHeaderTypes.HeaderType::class.java) | ||
| val headerType2 = forge.aValueFrom(HostsWithHeaderTypes.HeaderType::class.java, exclude = listOf(headerType1)) | ||
|
|
||
| val mappedHeaderType1 = headerType1.toSelectedTracingPropagator() | ||
| val mappedHeaderType2 = headerType2.toSelectedTracingPropagator() | ||
|
|
||
| val hostsWithHeaderTypes = HostsWithHeaderTypes( | ||
| hosts = mapOf( | ||
| host1 to setOf(headerType1), | ||
| host2 to setOf(headerType2, headerType1), | ||
| host3 to emptySet() | ||
| ) | ||
| ) | ||
|
|
||
| val result = HostsWithHeaderTypesMapper.mapToSelectedTracingPropagators(hostsWithHeaderTypes) | ||
|
|
||
| assertThat(result) | ||
| .containsExactlyInAnyOrder(mappedHeaderType1, mappedHeaderType2) | ||
| .doesNotHaveDuplicates() | ||
| } |
There was a problem hiding this comment.
| val host1 = forge.aString() | |
| val host2 = forge.aString() | |
| val host3 = forge.aString() | |
| val headerType1 = forge.aValueFrom(HostsWithHeaderTypes.HeaderType::class.java) | |
| val headerType2 = forge.aValueFrom(HostsWithHeaderTypes.HeaderType::class.java, exclude = listOf(headerType1)) | |
| val mappedHeaderType1 = headerType1.toSelectedTracingPropagator() | |
| val mappedHeaderType2 = headerType2.toSelectedTracingPropagator() | |
| val hostsWithHeaderTypes = HostsWithHeaderTypes( | |
| hosts = mapOf( | |
| host1 to setOf(headerType1), | |
| host2 to setOf(headerType2, headerType1), | |
| host3 to emptySet() | |
| ) | |
| ) | |
| val result = HostsWithHeaderTypesMapper.mapToSelectedTracingPropagators(hostsWithHeaderTypes) | |
| assertThat(result) | |
| .containsExactlyInAnyOrder(mappedHeaderType1, mappedHeaderType2) | |
| .doesNotHaveDuplicates() | |
| } | |
| // Given | |
| val host1 = forge.aString() | |
| val host2 = forge.aString() | |
| val host3 = forge.aString() | |
| val headerType1 = forge.aValueFrom(HostsWithHeaderTypes.HeaderType::class.java) | |
| val headerType2 = forge.aValueFrom(HostsWithHeaderTypes.HeaderType::class.java, exclude = listOf(headerType1)) | |
| val mappedHeaderType1 = headerType1.toSelectedTracingPropagator() | |
| val mappedHeaderType2 = headerType2.toSelectedTracingPropagator() | |
| val hostsWithHeaderTypes = HostsWithHeaderTypes( | |
| hosts = mapOf( | |
| host1 to setOf(headerType1), | |
| host2 to setOf(headerType2, headerType1), | |
| host3 to emptySet() | |
| ) | |
| ) | |
| // When | |
| val result = HostsWithHeaderTypesMapper.mapToSelectedTracingPropagators(hostsWithHeaderTypes) | |
| // Then | |
| assertThat(result) | |
| .containsExactlyInAnyOrder(mappedHeaderType1, mappedHeaderType2) | |
| .doesNotHaveDuplicates() | |
| } |
| .hasSelectedTracingPropagators( | ||
| internalConfigurationEvent.hostsWithHeaderTypes | ||
| ?.let(HostsWithHeaderTypesMapper::mapToSelectedTracingPropagators) | ||
| ) |
There was a problem hiding this comment.
if we drop hosts from HostsWithHeaderTypes, then we don't need mapToSelectedTracingPropagators call and can probably have simply .hasSelectedTracingPropagators(internalConfigurationEvent.hostsWithHeaderTypes) call
There was a problem hiding this comment.
No, I don't think so.
selectedTracingPropagatorsfield ofTelemetryConfigurationEventhas the typeList<SelectedTracingPropagator>.SelectedTracingPropagatoris located indd-sdk-android-rumand is not available indd-sdk-android-corewhereInternalTelemetryEvent.Configurationis constructed.- We still have to pass a set of enums in
internalConfigurationEvent.hostsWithHeaderTypesand then map it toList<SelectedTracingPropagator>in theTelemetryEventHandler. - And the same mapping has to be done in tests.
- We could make
internalConfigurationEvent.hostsWithHeaderTypesaSet<String>and then constructSelectedTracingPropagatorfromString. But I think it is a bad approach.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #2572 +/- ##
========================================
Coverage 70.20% 70.20%
========================================
Files 804 808 +4
Lines 30218 30224 +6
Branches 5052 5056 +4
========================================
+ Hits 21213 21218 +5
+ Misses 7602 7599 -3
- Partials 1403 1407 +4
🚀 New features to boost your workflow:
|
fc2ef36 to
067ef5f
Compare
| package com.datadog.android.internal.telemetry | ||
|
|
||
| @Suppress("UndocumentedPublicClass", "UndocumentedPublicProperty") | ||
| data class InternalTracingHeaderTypesSet( |
There was a problem hiding this comment.
do you want to keep this class with only one property just for the possible extension in the future? Otherwise we can drop this class and just use Set<InternalTracingHeaderType> directly
There was a problem hiding this comment.
do you want to keep this class with only one property just for the possible extension in the future?
No, it's not about extending. The only purpose of this class is to be able to do an easy cast here. Otherwise I would have to first do as? Set<*> and then check every element of the set that it is of the needed enum type. Or do an unchecked cast.
| package com.datadog.android.internal.telemetry | ||
|
|
||
| @Suppress("UndocumentedPublicClass") | ||
| enum class InternalTracingHeaderType { |
There was a problem hiding this comment.
idea: maybe drop Internal prefix from the name? Is it useful to have it?
Same for the InternalTracingHeaderTypesSet.
Also dd-sdk-android-okhttp already depends on the dd-sdk-android-rum module, so we can reference SelectedTracingPropagator from there directly, without having a dedicated intermediate enum with basically same values. Is it useful to have it?
There was a problem hiding this comment.
good point, I'll try to do this
There was a problem hiding this comment.
ok, I thought about it
maybe drop Internal prefix from the name? Is it useful to have it?
My idea was to distinguish it from an already existing TracingHeaderType. link which is public and can be used by clients of the sdk. Thus the prefix - internal.
Also dd-sdk-android-okhttp already depends on the dd-sdk-android-rum module, so we can reference SelectedTracingPropagator from there directly, without having a dedicated intermediate enum with basically same values. Is it useful to have it?
Now these header types are passed to InternalTelemetryEvent.Configuration here. It is in the dd-sdk-android-internal and can't access neighter TracingHeaderType nor SelectedTracingPropagator.
There are 2 choices:
- Continue using
InternalTelemetryEvent.Configuration, but pass theSet<SelectedTracingPropagator>asAny. Not the best because we haveAny. - Extract the set of headers right in the
TelemetryEventHandlersimilar to this. Not the best because we inTelemetryEventHandler(rum feature) we rely on the fact that there is tracing feature somewhere (features start knowing about one another).
I like approach with InternalTelemetryEvent.Configuration (the way I did it in pr, not with Any) because:
- It uses static typing (at least in the
TelemetryConfigurationHandler). - The code in
TelemetryConfigurationHandler.createConfigurationEventis "cleaner" - just a function that acceptsInternalTelemetryEvent.Configurationas parameter and creates an event (if we don't take this into account).
I suggest 2 options:
- Either I leave it the way it is now (
InternalTracingHeaderTyperemains). - Stop putting things in
InternalTelemetryEvent.Configurationand just extract everything from different features' contexts inTelemetryEventHandler. I can even change my implementation forokhttpInterceptorSampleRatefrom the previous pr.
I like option 1 more. The fact that we have a duplicate for TracingHeaderType isn't a big problem imho. Duplication - yes. But we have kind of bigger separation of public api TracingHeaderType and internal api InternalTracingHeaderType. There is close to no risk that we forget to change TracingHeaderType if InternalTracingHeaderType changes because it won't compile. And we can change InternalTracingHeaderType more freely because it is not public api. Maybe dd-sdk-android-internal will become a place where we store shared models that can be accessed by all features instead of using HashMaps to communicate between modules.
But I have no problem refactoring to option 2.
There was a problem hiding this comment.
My idea was to distinguish it from an already existing TracingHeaderType. link which is public and can be used by clients of the sdk. Thus the prefix - internal.
What is being added already have a different package name (with internal in the path) and moreover it is a part of dd-sdk-android-internal, which is added as implementation everywhere, so it is not re-exported to the public API.
I suggest 2 options:
Let's do option 1 then, I'm fine with that.
There was a problem hiding this comment.
What is being added already have a different package name (with internal in the path) and moreover it is a part of dd-sdk-android-internal, which is added as implementation everywhere, so it is not re-exported to the public API.
I will clarify a bit. I mean that when we (sdk devs) try to search for the class in Android Studio, we don't see 2 classes with exactly the same name. I understand that dd-sdk-android-internal is added as implementation everywhere and its classes are not visible to the clients of the sdk.
There was a problem hiding this comment.
Let's do option 1 then, I'm fine with that.
done
idea: maybe drop Internal prefix from the name? Is it useful to have it?
done, droped internal prefix from both TracingHeaderType and TracingHeaderTypesSet
| import fr.xgouchet.elmyr.Forge | ||
| import fr.xgouchet.elmyr.ForgeryFactory | ||
|
|
||
| class HostsWithHeaderTypesForgeryFactory : ForgeryFactory<InternalTracingHeaderTypesSet> { |
There was a problem hiding this comment.
this class name should be InternalTracingHeaderTypesSetForgeryFactory then
| fun InternalTracingHeaderTypesSet.toSelectedTracingPropagators() = types.map { it.toSelectedTracingPropagator() } | ||
|
|
||
| private fun InternalTracingHeaderType.toSelectedTracingPropagator(): |
There was a problem hiding this comment.
suggestion: maybe make them 1st class citizens? without HostsWithHeaderTypesMapper object wrapper (which should also be renamed). Given that they have custom receiver, it seems there is no benefit.
There was a problem hiding this comment.
removed the objects that I created, changed to top-level functions
| fun `M add trace sample rate to tracing feature context W constructor called`( | ||
| fun `M add trace sample rate and header types to tracing feature context W constructor called`( |
There was a problem hiding this comment.
I don't see why I should. The test does verifyNoMoreInteractions in the end. If I split it in two, the separate tests would not be able to do that.
If you have some specific idea in mind, please tell me.
| import com.datadog.android.internal.telemetry.InternalTracingHeaderTypesSet | ||
| import com.datadog.android.trace.TracingHeaderType | ||
|
|
||
| internal object TracingHeaderTypeMapper { |
There was a problem hiding this comment.
In my opinion, We try to avoid to use Kotlin object if it doesn't have a strong argument in our SDK, what's the necessity of this one?
There was a problem hiding this comment.
No problem, I will remove the objects. To me it is just a question of style and preference.
There was a problem hiding this comment.
For our SDK it's just not just a style, the main problem of using object is that it's hard to mock it since we don't use mockk for static mocking. And it also stays globally in the memory even if it is never used.
e8a2407 to
82d8e97
Compare
0xnm
left a comment
There was a problem hiding this comment.
lgtm, just some minor naming update suggestions
| internal val startupTimeNs: Long = System.nanoTime() | ||
|
|
||
| internal const val OKHTTP_INTERCEPTOR_SAMPLE_RATE = "okhttp_interceptor_sample_rate" | ||
| internal const val OKHTTP_INTERCEPTOR_HOSTS_WITH_HEADER_TYPES = "okhttp_interceptor_hosts_with_header_types" |
There was a problem hiding this comment.
minor: there is no hosts anymore
| internal const val OKHTTP_INTERCEPTOR_HOSTS_WITH_HEADER_TYPES = "okhttp_interceptor_hosts_with_header_types" | |
| internal const val OKHTTP_INTERCEPTOR_HEADER_TYPES = "okhttp_interceptor_header_types" |
| class com.datadog.android.sqlite.DatadogDatabaseErrorHandler : android.database.DatabaseErrorHandler | ||
| constructor(String? = null, android.database.DatabaseErrorHandler = DefaultDatabaseErrorHandler()) | ||
| override fun onCorruption(android.database.sqlite.SQLiteDatabase) | ||
| fun com.datadog.android.internal.telemetry.TracingHeaderTypesSet.toSelectedTracingPropagators() |
| internal const val W3C_PARENT_ID_LENGTH = 16 | ||
|
|
||
| internal const val OKHTTP_INTERCEPTOR_SAMPLE_RATE = "okhttp_interceptor_sample_rate" | ||
| internal const val OKHTTP_INTERCEPTOR_HOSTS_WITH_HEADER_TYPES = "okhttp_interceptor_hosts_with_header_types" |
There was a problem hiding this comment.
| internal const val OKHTTP_INTERCEPTOR_HOSTS_WITH_HEADER_TYPES = "okhttp_interceptor_hosts_with_header_types" | |
| internal const val OKHTTP_INTERCEPTOR_HEADER_TYPES = "okhttp_interceptor_header_types" |
There was a problem hiding this comment.
thanks for spotting, fixed
…figuration telemetry
82d8e97 to
68b4789
Compare
What does this PR do?
We provide distributed tracing header types to the
selectedTracingPropagatorsfield of theTelemetryConfigurationEventin a similar way we did with thetraceSampleRatehere #2563. The disadvantages described in that pr also apply here.demo [internal]:
link logs from the sample app. Select at least 1 week window so that you can see events from Friday March 28th.
Motivation
What inspired you to submit this pull request?
Additional Notes
Anything else we should know when reviewing?
Review checklist (to be filled by reviewers)