Skip to content

Commit 6b142cc

Browse files
Add documentation for new InstrumentationDefinitions generator (DataDog#2310)
* Add links to our other docs from Tracer README * Add documentation for new InstrumentationDefinitions generator Made this more of a general "how to create an integration" document, as well as including the details about the new attributes. Maybe got carried away? * Don't run build pipeline on changes to the new markdown file * Don't run consolidated pipeline on changes to noop pipeline! * Apply suggestions from code review Co-authored-by: Pierre Bonet <pierre.bonet@datadoghq.com> Co-authored-by: Pierre Bonet <pierre.bonet@datadoghq.com>
1 parent 9c81234 commit 6b142cc

File tree

4 files changed

+129
-3
lines changed

4 files changed

+129
-3
lines changed

.azure-pipelines/noop-pipeline.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ pr:
55
exclude:
66
- .
77
include:
8-
- blog/
98
- docs/
109
- tracer/README.MD
1110
- tracer/src/Datadog.Trace/DuckTyping/README.md
11+
- tracer/src/Datadog.Trace/ClrProfiler/README.md
1212
- tracer/samples
1313
- .github/
1414
- LICENSE
1515
- LICENSE-3rdparty.csv
1616
- NOTICE
17+
- .azure-pipelines/noop-pipeline.yml
1718

1819

1920
# Global variables

.azure-pipelines/ultimate-pipeline.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ trigger:
1313
- tracer/README.MD
1414
- .github/*
1515
- tracer/dependabot/*
16-
- tracer/src/Datadog.Trace/DuckTyping/README.md
17-
- blog/*
16+
- "*.md"
1817
- LICENSE
1918
- LICENSE-3rdparty.csv
2019
- NOTICE
20+
- .azure-pipelines/noop-pipeline.yml
2121
# The following is a list of shared asset locations.
2222
# This is the config for the Tracer CI pipeline,
2323
# so we are excluding shared assets that are not currently used by the Tracer.

tracer/README.MD

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ You can use Rider and CLion to develop on macOS. Building and testing can be don
9999
.\build.sh BuildAndRunIntegrationTests
100100
```
101101

102+
## Additional Technical Documentation
103+
104+
* [Implementing an automatic instrumentation](./src/Datadog.Trace/ClrProfiler/README.md)
105+
* [Duck typing: usages, best practices, and benchmarks](./src/Datadog.Trace/DuckTyping/README.md)
106+
* [Datadog.Trace NuGet package README](../docs/Datadog.Trace/README.md)
107+
102108
## Further Reading
103109

104110
Datadog APM
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
## Automatic Instrumentation
2+
3+
The _ClrProfiler_ folder contains the majority of code required for automatic instrumentation of target methods. Since v2.0.0, we exclusively use "Call Target" modification, in which we rewrite the target method to add our instrumentation.
4+
5+
### Creating a new automatic instrumentation implementation
6+
7+
Creating a new instrumentation implementation typically uses the following process:
8+
9+
1. Identify the operation of interest that we want to measure. Also gather the tags, resource names that we will need to set. Don't forget to check what has been implemented by other tracers.
10+
2. Find an appropriate instrumentation point in the target library. You may need to use multiple instrumentation points, and you may need to use different targets for different _versions_ of the library
11+
3. Create an instrumentation class using one of the standard "shapes" (described below), and place it in the [ClrProfiler/AutoInstrumentation folder](./AutoInstrumentation). If the methods you need to instrument have different prototypes (especially the number of parameters), you will need multiple class to instrument them.
12+
4. Add an `[InstrumentMethod]` attribute to the instrumentation class, as described below. Alternatively, add an assembly-level `[AdoNetClientInstrumentMethods]` attribute
13+
5. (Optional) Create duck-typing unit tests in _Datadog.Trace.Tests_ to confirm any duck types are valid. This can make the feedback cycle much faster than relying on integration tests
14+
6. Create integration tests for your instrumentation
15+
1. Create (or reuse) a sample application that uses the target library, which ideally exercises all the code paths in your new instrumentation. Use an `$(ApiVersion)` MSBuild variables to allow testing against multiple package versions in CI.
16+
2. Add an entry in [tracer/build/PackageVersionsGeneratorDefinitions.json](../../../build/PackageVersionsGeneratorDefinitions.json) defining the range of all supported versions. See the existing definitions for examples
17+
3. Run `./tracer/build.ps1 GeneratePackageVersions`. This generates the xunit test data for package versions in the `TestData` that you can use as `[MemberData]` for your `[Theory]` tests.
18+
4. Use the `MockTracerAgent` to confirm your instrumentation is working as expected.
19+
7. After testing locally, push to GitHub, and do a manual run in Azure Devops for your branch
20+
1. Navigate to the [consolidated-pipeline](https://dev.azure.com/datadoghq/dd-trace-dotnet/_build?definitionId=54)
21+
2. Click `Run Pipeline`
22+
3. Select your branch from the drop down
23+
4. Click `Variables`, set `perform_comprehensive_testing` to true. (This is false for PRs by default for speed, but ensures your new code is tested against all the specified packages initially)
24+
5. Select `Stages To Run`, and select only the `build*`, `unit_test*` and `integration_test*` stages. This avoids using excessive resources, and will complete your build faster
25+
8. Once your test branch works, create a PR!
26+
27+
### Instrumentation classes
28+
29+
When implementing instrumentation classes, you can run code both _before_ the target method is entered, and _after_ it is entered. Your `OnMethodBegin` method will always look the same, but the shape of the `OnMethodEnd` depends on whether the method is async, and whether it returns void:
30+
31+
```csharp
32+
public class ClientQueryIteratorsIntegrations
33+
{
34+
// The parameters here should match the method signature of the target method
35+
// Use generic parameters for non-BCL types that you can't directly reference
36+
internal static CallTargetState OnMethodBegin<TTarget, TOther>(TTarget instance, TOther otherParam)
37+
{
38+
// Run your "method start" code here
39+
}
40+
41+
// Include ONE of the following:
42+
43+
// 👇 Async method
44+
internal static TReturn OnAsyncMethodEnd<TTarget, TReturn>(TTarget instance, TReturn returnValue, Exception exception, in CallTargetState state)
45+
{
46+
state.Scope?.DisposeWithException(exception);
47+
return returnValue;
48+
}
49+
50+
// 👇 Method with return value TReturn
51+
internal static CallTargetReturn<TReturn> OnMethodEnd<TTarget, TReturn>(TTarget instance, TReturn returnValue, Exception exception, in CallTargetState state)
52+
{
53+
// Run your "method end" code here
54+
}
55+
56+
// 👇 Void method
57+
internal static CallTargetReturn OnMethodEnd<TTarget>(TTarget instance, Exception exception, in CallTargetState state)
58+
{
59+
// Run your "method end" code here
60+
}
61+
}
62+
```
63+
64+
### Instrumentation attributes
65+
66+
A source generator is used to automatically "find" all custom instrumentation classes in the app and generate a list of them to pass to the native CLR profiler. We do this by using one of two attributes:
67+
68+
- [`[InstrumentMethod]`](./InstrumentMethodAttribute.cs)
69+
- [`[AdoNetClientInstrumentMethods]`](./AutoInstrumentation/AdoNet/AdoNetClientInstrumentMethodsAttribute.cs)
70+
71+
> Alternatively, you can _manually_ call `NativeMethods.InitializeProfiler()`, passing in `NativeCallTargetDefinition[]`. This is not the "normal" approach, but may be necessary when you need to dynamically generate definitions, for example in serverless scenarios
72+
73+
In most cases, you will want `[InstrumentMethod]`. You apply this to your instrumentation class, describing the target assembly, method, instrumentation name etc. For example:
74+
75+
```csharp
76+
[InstrumentMethod(
77+
AssemblyName = "System.Net.Http",
78+
TypeName = "System.Net.Http.HttpClientHandler",
79+
MethodName = "SendAsync",
80+
ReturnTypeName = ClrNames.HttpResponseMessageTask,
81+
ParameterTypeNames = new[] { ClrNames.HttpRequestMessage, ClrNames.CancellationToken },
82+
MinimumVersion = "4.0.0",
83+
MaximumVersion = "6.*.*",
84+
IntegrationName = IntegrationName)]
85+
public class HttpClientHandlerIntegration
86+
{
87+
// ...
88+
}
89+
```
90+
91+
We also have a special case for ADO.NET method instrumentation, as this is generally more convoluted, and requires a lot of duplication. All new ADO.NET implementations will likely reuse existing instrumentation classes, such as [`CommandExecuteReaderIntegration`](./AutoInstrumentation/AdoNet/CommandExecuteReaderIntegration.cs) for example. To save having to specify many `[InstrumentMethod]` attributes, you can instead use the `[AdoNetClientInstrumentMethods]` _assembly_ attribute, to define some standard types, as well as which of the standard ADO.NET signatures to implement. For example:
92+
93+
```csharp
94+
[assembly: AdoNetClientInstrumentMethods(
95+
AssemblyName = "MySql.Data",
96+
TypeName = "MySql.Data.MySqlClient.MySqlCommand",
97+
MinimumVersion = "6.7.0",
98+
MaximumVersion = "6.*.*",
99+
IntegrationName = nameof(IntegrationId.MySql),
100+
DataReaderType = "MySql.Data.MySqlClient.MySqlDataReader",
101+
DataReaderTaskType = "System.Threading.Tasks.Task`1<MySql.Data.MySqlClient.MySqlDataReader>",
102+
TargetMethodAttributes = new[]
103+
{
104+
// int MySql.Data.MySqlClient.MySqlCommand.ExecuteNonQuery()
105+
typeof(CommandExecuteNonQueryAttribute),
106+
// MySqlDataReader MySql.Data.MySqlClient.MySqlCommand.ExecuteReader()
107+
typeof(CommandExecuteReaderAttribute),
108+
// MySqlDataReader MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior)
109+
typeof(CommandExecuteReaderWithBehaviorAttribute),
110+
// DbDataReader MySql.Data.MySqlClient.MySqlCommand.ExecuteDbDataReader(CommandBehavior)
111+
typeof(CommandExecuteDbDataReaderWithBehaviorAttribute),
112+
// object MySql.Data.MySqlClient.MySqlCommand.ExecuteScalar()
113+
typeof(CommandExecuteScalarAttribute),
114+
})]
115+
```
116+
117+
The above attribute shows how to select which signatures to implement, via the `TargetMethodAttributes` property. These attributes are nested types defined inside [`AdoNetClientInstrumentMethodsAttribute`](./AutoInstrumentation/AdoNet/AdoNetClientInstrumentMethodsAttribute.cs), each of which are associated with a given signature + instrumentation class (via the `[AdoNetClientInstrumentMethodsAttribute.AdoNetTargetSignature]` attribute)
118+
119+
> Note that there are separate target method attributes if you are using the new abstract/interface instrumentation feature.

0 commit comments

Comments
 (0)