|
| 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