No-code instrumentation for .NET in Splunk Observability Cloud
This document explains how to instrument a .NET application using no‑code, configuration‑based instrumentation.
Overview
No‑code instrumentation for .NET lets you collect spans and method‑level telemetry without modifying your application's source code. Instead of adding manual instrumentation, you define the methods you want to instrument in a YAML configuration file. At runtime, the .NET instrumentation agent automatically applies these rules and adds the necessary logic to create spans, capture attributes, and enrich your observability data.
This approach is especially useful when you need to instrument third‑party libraries, legacy applications, or large codebases where changing the source code is difficult or not possible.
This feature is experimental. The configuration schema and behavior may change in future releases.
Key capabilities
No-code instrumentation for .NET provides a flexible, configuration-driven way to instrument methods across your application and external libraries without modifying source code. The following capabilities are supported:
- Universal method instrumentation
You can instrument almost any method in any .NET assembly — your own code, shared libraries, or third-party packages — without touching the source code.
- Flexible method targeting
You're free to instrument many kinds of methods: static, instance, async, generic, and even overloaded ones. If your application can call it, you can most likely instrument it.
- Parameter support
Methods with up to nine parameters are supported, so you can target a wide range of real-world method signatures.
- Return type support
You can instrument methods that return anything from
voidand simple types toTask,Task<T>,ValueTask,ValueTask<T>, or custom classes. - Span customization
You decide what the span looks like — name, kind, and any custom attributes — all defined directly in your YAML configuration.
- Method overload support
If a method has multiple overloads, you can target the exact one you want by specifying its full signature.
Configuration structure
The no-code instrumentation feature is configured under the no_code/development section of your YAML configuration file. This section defines a list of instrumentation rules that specify which methods should be instrumented and how spans should be created when those methods are invoked.
Each rule describes a single target method, including its assembly, type, method name, and signature, along with the span settings applied during execution.
Target fields
The target section identifies the exact method to instrument. These fields ensure that the correct method is matched, even when multiple overloads exist.
assembly.nameThe name of the assembly that contains the method.
typeThe fully qualified class name that defines the method.
methodThe method name.
signature.return_typeThe return type of the method.
signature.parameter_typesA list of parameter types used to match the correct overload.
Span fields
The span section defines how the span should be created when the target method is invoked.
- name
A custom span name. If omitted, the method name is used.
- kind
The span kind (for example:
internal,server,client,producer,consumer). - attributes
A list of custom attributes added to the span.
Attribute types
The following attribute types are supported. Use them to ensure that span attributes are recorded correctly and not dropped during processing.
string- Text values such as identifiers, names, or descriptive labels.bool- Boolean values (true/false) used for flags or binary states.int- Whole numbers, typically used for counters, sizes, or numeric identifiers.double- Floating-point numbers for measurements or values requiring decimal precision.string_array- An array of text values, useful for lists such as tags or categories.bool_array- An array of boolean values.int_array- An array of integers, often used for numeric collections or ranges.double_array- An array of floating-point numbers for sets of measurements.
Dynamic attributes
Dynamic attributes allow you to extract attribute values from the method context at runtime using an expression syntax based on CEL (Common Expression Language). Instead of specifying static values, use the value_source property with expressions to dynamically obtain attribute values. This section consolidates expression syntax, dynamic span names, and status configuration into a single comprehensive overview.
Expression syntax
- Identifiers
-
Access method execution context:
Identifier Description Example argumentsArray of method arguments (zero-indexed) arguments[0]instanceThe instance object (for instance methods) instance.ServiceNamereturnMethod return value (in OnMethodEnd) return.StatusCodemethodMethod name methodtypeDeclaring type name type - Member Access
-
Access properties and array elements:
Expression Description arguments[0]First method argument arguments[1]Second method argument arguments[0].PropertyNameProperty of first argument arguments[0].Nested.PropertyNested property access instance.PropertyNameProperty of instance object return.ResultPropertyProperty of return value - Operators
-
Compare and combine values:
Operator Description Example ==Equality arguments[0] == "expected"!=Inequality return.StatusCode != 200<,>Comparison arguments[0].Age > 18<=,>=Comparison return.Score >= 50&&Logical AND arguments[0] != null && return.Success||Logical OR return.Success || return.Retryable!Logical NOT !return.HasError? :Ternary conditional return.Success ? "ok" : "failed"Note:- Arguments are zero-indexed (
arguments[0]toarguments[8]) - Property access uses reflection and only works with public properties
- If an expression evaluates to
null, the attribute is omitted - Invalid expressions or property paths are silently skipped (logged at debug level)
- Expressions are parsed and validated at configuration load time
- This DSL implements a subset of CEL; not all CEL features are supported
- Arguments are zero-indexed (
- Truthy Values
-
In boolean contexts (such as conditions in status rules, ternary operators, and logical operators), values are evaluated for truthiness according to their type.
The following example demonstrates truthy values in both attributes and status rules:
Type Truthy Condition Examples boolThe value itself trueis truthy,falseis falsystringNon-null and non-empty "text"is truthy,""is falsybyteNon-zero 1is truthy,0is falsysbyteNon-zero 1is truthy,0is falsyshortNon-zero 1is truthy,0is falsyushortNon-zero 1is truthy,0is falsyintNon-zero 1is truthy,0is falsyuintNon-zero 1uis truthy,0uis falsylongNon-zero 1Lis truthy,0Lis falsyulongNon-zero 1ULis truthy,0ULis falsyfloatAbsolute value > 0 1.23fis truthy,0.0fis falsydoubleAbsolute value > 0 1.23is truthy,0.0is falsydecimalNon-zero 1.5mis truthy,0mis falsynullAlways falsy Always treated as false Other objects Non-null objects are truthy Custom objects are truthy if not null, falsy if null CODEspan: name: process-data attributes: # Using ternary for safe defaults - name: user.name value_source: "arguments[0].Name ? arguments[0].Name : \"unknown\"" type: string status: rules: # Return value is truthy (non-null, non-empty, non-zero) - condition: "return" code: ok # Return value is falsy - condition: "!return" code: error # String property is non-empty - condition: "return.ErrorMessage" code: error description: "Error message present" # Numeric property is non-zero - condition: "return.StatusCode" code: ok
Dynamic attribute examples
- Method arguments
-
CODE
attributes: - name: order.id value_source: "arguments[0]" # First argument value type: int - name: customer.id value_source: "arguments[1]" # Second argument value type: string - Argument properties
-
CODE
attributes: - name: request.url value_source: "arguments[0].RequestUri.AbsoluteUri" type: string - name: user.email value_source: "arguments[0].User.Email" type: string - Instance values
-
CODE
attributes: - name: service.name value_source: "instance.ServiceName" type: string - name: merchant.id value_source: "instance.MerchantId" type: string - Conditional logic
-
CODE
attributes: - name: user.type value_source: "arguments[0].Age >= 18 ? \"adult\" : \"minor\"" type: string - name: status value_source: "return.Success ? \"ok\" : \"failed\"" type: string
Functions
The expression DSL supports functions for transforming and combining values. Functions can be used in the value_source property for attributes, in status rule conditions, or in the name_source property for dynamic span names.
Supported functions
| Function | Description | Example |
|---|---|---|
string(value) |
Convert value to string | string(arguments[0].Id) |
size(value) |
Get length/count of string, list, or map | size(arguments[0].Items) |
startsWith(str, prefix) |
Check if string starts with prefix | startsWith(arguments[0].Path, "/api/") |
endsWith(str, suffix) |
Check if string ends with suffix | endsWith(arguments[0].FileName, ".json") |
contains(str, substring) |
Check if string contains substring | contains(arguments[0].Message, "error") |
startsWith, endsWith, contains) use ordinal comparison (case-sensitive), equivalent to StringComparison.Ordinal in C#.
Function expression examples
- Concatenate values
-
CODE
attributes: - name: operation.id value_source: "type + \".\" + method" type: string - name: order.key value_source: "arguments[0].CustomerId + \"-\" + arguments[0].OrderId" type: string - Use ternary operator for defaults
-
CODE
attributes: - name: user.name value_source: "arguments[0].DisplayName != null ? arguments[0].DisplayName : \"anonymous\"" type: string - String operations
-
CODE
attributes: - name: is.api.request value_source: "startsWith(arguments[0].Path, \"/api/\")" type: bool - name: is.json.file value_source: "endsWith(arguments[0].FileName, \".json\")" type: bool - name: has.error value_source: "contains(return.Message, \"error\")" type: bool - Convert and measure
-
CODE
attributes: - name: item.count.string value_source: "string(size(arguments[0].Items))" type: string - name: name.length value_source: "size(arguments[0].Name)" type: int
Dynamic span names
name_source property with an expression to specify the dynamic span name. The name property is still required as a fallback if the dynamic expression fails to evaluate.
+ operator to combine values into a meaningful string. This ensures the result is always a string type.
Dynamic span name examples
- Create span names from argument values
-
CODE
span: name: DefaultTransaction # Fallback name name_source: "\"Transaction-\" + arguments[0]" # Dynamic name using first argument - Combine multiple values
-
CODE
span: name: DefaultQuery # Fallback name name_source: "\"Query.\" + arguments[0] + \".\" + arguments[1]" # e.g., "Query.ProductionDB.users" - Include method context
-
CODE
span: name: DefaultOperation # Fallback name name_source: "method + \"-\" + arguments[0].OperationType" # e.g., "ProcessOrder-Express" - Use with nested properties
-
CODE
span: name: DefaultRequest # Fallback name name_source: "arguments[0].HttpMethod + \" \" + arguments[0].Path" # e.g., "GET /api/users" - Use conditional expressions
-
CODE
span: name: DefaultOrder # Fallback name name_source: "arguments[0].Amount > 1000 ? \"LargeOrder\" : \"Order\""
Status configuration
You can configure span status dynamically based on method execution results using expressions. Status rules allow you to classify operations as successful or failed without modifying application code.
Rules are evaluated in order, and the first matching rule determines the final span status.
Status rule syntax
span:
name: my-span
status:
rules:
- condition: <expression> # Expression that evaluates to a boolean value
code: <status_code> # ok, error, or unset
description: <text> # Optional static description
- ok — The operation completed successfully.
- error — The operation failed.
- unset — No status is set (default).
Examples
The following example demonstrates a basic rule for setting error status based on a return value:
span:
name: process-order
status:
rules:
- condition: "return.Success == false"
code: error
description: "Order processing failed"
- condition: "return.Success"
code: ok
The following example demonstrates a complete configuration with dynamic attributes and status rules:
instrumentation/development:
dotnet:
no_code:
targets:
- target:
assembly:
name: MyApp.Services
type: MyApp.Services.OrderService
method: ProcessOrder
signature:
return_type: MyApp.Models.OrderResult
parameter_types:
- MyApp.Models.OrderRequest
span:
name: process-order
kind: internal
attributes:
- name: order.id
value_source: "arguments[0].OrderId"
type: string
- name: customer.id
value_source: "arguments[0].CustomerId"
type: string
- name: order.total
value_source: "arguments[0].TotalAmount"
type: double
status:
rules:
- condition: "return == null"
code: error
description: "Null result returned"
- condition: "return.Status == \"Failed\""
code: error
description: "Order processing failed"
- condition: "return.Status == \"Completed\""
code: ok
- Conditions must evaluate to a boolean value.
- Rules are processed sequentially. Only the first matching rule is applied.
- The
returnvalue is available only after method execution completes. - If no rule matches, the span status remains
unset.
Example configuration
The following examples illustrates what a no-code instrumentation rule may look like in your YAML configuration:
- Basic method instrumentation
-
Instrument a simple static method:
CODEpublic static void TestMethodStatic();Configuration:
CODEinstrumentation/development: dotnet: no_code: targets: - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: TestMethodStatic signature: return_type: System.Void parameter_types: span: name: Span-TestMethodStatic kind: internal - Instrumentation with default span kind
-
When
kindis omitted, it defaults tointernal:CODEpublic void TestMethodA();Configuration:
CODEinstrumentation/development: dotnet: no_code: targets: - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: TestMethodA signature: return_type: System.Void parameter_types: span: name: Span-TestMethodA # kind defaults to 'internal' when omitted - Method with parameters
-
Instrument a method with specific parameters:
CODEpublic void TestMethod(string param1, string param2);Configuration:
CODEinstrumentation/development: dotnet: no_code: targets: - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: TestMethod signature: return_type: System.Void parameter_types: - System.String - System.String span: name: Span-TestMethod2 kind: server attributes: - name: operation.type value: "test_method" type: string - Async method instrumentation
-
Instrument an async method returning
Task<T>:CODEpublic async Task<int> IntTaskTestMethodAsync();Configuration:
CODEinstrumentation/development: dotnet: no_code: targets: - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: IntTaskTestMethodAsync signature: return_type: System.Threading.Tasks.Task`1[System.Int32] parameter_types: span: name: Span-IntTaskTestMethodAsync kind: client attributes: - name: async.operation value: "task_with_return" type: string - Multiple attributes with different types
-
Configure spans with various attribute types (from the actual test configuration):
CODEpublic static void TestMethodStatic();Configuration with multiple attribute types:
CODEinstrumentation/development: dotnet: no_code: targets: - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: TestMethodStatic signature: return_type: System.Void parameter_types: span: name: Span-TestMethodStatic kind: internal attributes: - name: attribute_key_string value: "string_value" type: string - name: attribute_key_bool value: true type: bool - name: attribute_key_int value: 12345 type: int - name: attribute_key_double value: 123.45 type: double - name: attribute_key_string_array value: ["value1", "value2", "value3"] type: string_array - name: attribute_key_bool_array value: [true, false, true] type: bool_array - name: attribute_key_int_array value: [123, 456, 789] type: int_array - name: attribute_key_double_array value: [123.45, 678.90] type: double_array - Overload targeting
-
Target specific method overloads by parameter types:
CODE// Parameterless overload public void TestMethod(); // String parameter overload public void TestMethod(string param1); // Int parameter overload public void TestMethod(int param1);Configuration for targeting specific overloads:
CODEinstrumentation/development: dotnet: no_code: targets: # Parameterless overload - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: TestMethod signature: return_type: System.Void parameter_types: span: name: Span-TestMethod0 kind: client # String parameter overload - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: TestMethod signature: return_type: System.Void parameter_types: - System.String span: name: Span-TestMethod1String kind: producer # Int parameter overload - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: TestMethod signature: return_type: System.Void parameter_types: - System.Int32 span: name: Span-TestMethod1Int kind: server - ValueTask Support (.NET 8+ only)
-
Instrument methods returning
ValueTaskorValueTask<T>:CODEpublic async ValueTask<int> IntValueTaskTestMethodAsync();Configuration:
CODEinstrumentation/development: dotnet: no_code: targets: - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: IntValueTaskTestMethodAsync signature: return_type: System.Threading.Tasks.ValueTask`1[System.Int32] parameter_types: span: name: Span-IntValueTaskTestMethodAsync kind: client - Generic method instrumentation
-
Instrument generic methods (note the return type specification):
CODEpublic T? GenericTestMethod<T>();Note: When instrumenting generic methods, make sure to specify the concrete types that result from generic type substitution. Both the return type and all parameter types must reflect the compiled method signature. To determine the exact types, use tools such as ILSpy or other IL viewers. In ILSpy, switch the view to IL with C# to inspect the fully resolved method signature.Configuration (when called as
GenericTestMethod<int>()):CODEinstrumentation/development: dotnet: no_code: targets: - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: GenericTestMethod signature: return_type: System.Int32 parameter_types: span: name: Span-GenericTestMethod kind: internal - Generic class instrumentation
-
Instrument methods in generic classes with class-level type parameters:
JAVApublic class GenericNoCodeTestingClass<TFooClass, TBarClass> { public TFooMethod GenericTestMethod<TFooMethod, TBarMethod>( TFooMethod fooMethod, TBarMethod barMethod, TFooClass fooClass, TBarClass barClass) { return fooMethod; } }For generic classes, use the backtick notation with the number of class-level type parameters:
Generic Type Parameter Notation:
- Class-level type parameters: Use
!!0,!!1, etc. (where!!0is the first class type parameter) - Method-level type parameters: Use
!0,!1, etc. (where!0is the first method type parameter) - In the type name, use backtick notation:
ClassName\N` where N is the number of generic parameters
Configuration:
CODEinstrumentation/development: dotnet: no_code: targets: - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.GenericNoCodeTestingClass`2 method: GenericTestMethod signature: return_type: '!0' parameter_types: - '!0' - '!1' - '!!0' - '!!1' span: name: Span-GenericTestMethodWithParameters kind: internalIn this example:
GenericNoCodeTestingClass\2` indicates a class with 2 generic type parameters'!0'represents the first method type parameter (TFooMethod)'!1'represents the second method type parameter (TBarMethod)'!!0'represents the first class type parameter (TFooClass)'!!1'represents the second class type parameter (TBarClass)
- Class-level type parameters: Use
- Methods with return values
-
Instrument methods that return values:
CODE// Method returning string public string ReturningStringTestMethod(); // Method returning custom class public TestClass ReturningCustomClassTestMethod();Configuration:
CODEinstrumentation/development: dotnet: no_code: targets: # Method returning string - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: ReturningStringTestMethod signature: return_type: System.String parameter_types: span: name: Span-ReturningStringTestMethod kind: internal # Method returning custom class - target: assembly: name: TestApplication.NoCode type: TestApplication.NoCode.NoCodeTestingClass method: ReturningCustomClassTestMethod signature: return_type: TestApplication.NoCode.TestClass parameter_types: span: name: Span-ReturningCustomClassTestMethod kind: internal
The repository includes a full test application that contains complete, working examples of all no‑code instrumentation scenarios. You can review the:
to see how each rule behaves in a real .NET environment.
Best practices
Apply the following best practices to ensure reliable, performant, and maintainable no-code instrumentation:
- Prefer manual instrumentation when feasible:
If the source code is accessible, manual instrumentation provides lower overhead, more granular control over span boundaries, and better alignment with application semantics.
- Define explicit and stable span names:
Use deterministic naming conventions that accurately represent the instrumented operation. Avoid ambiguous or context-dependent names.
- Select the correct span kind for the operation model:
Ensure that the configured span kind reflects the method’s functional role, such as
internal,server,client,producer, orconsumer. Incorrect span kinds can degrade trace interpretation. - Emit attributes that improve diagnostic value:
Add attributes that carry operational context, identifiers, or input/output metadata. Avoid attributes that are redundant, unstable, or high-cardinality unless strictly necessary.
- Target method overloads precisely:
Always specify the full method signature, including return type and parameter types, to avoid unintentionally instrumenting the wrong overload.
- Validate configuration in an isolated test environment:
Use a dedicated test application to confirm that spans are emitted as expected before rolling out changes to production.
- Evaluate performance impact on high-frequency code paths:
Instrumentation introduces overhead. For hot paths or tight loops, assess whether the diagnostic value justifies the cost, and consider sampling or selective instrumentation.