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.

Note:

This feature is experimental. The configuration schema and behavior may change in future releases.

CAUTION: No‑code instrumentation adds runtime overhead because it injects additional logic into instrumented methods. This may impact performance, especially for frequently called methods. When possible, prefer manual instrumentation for better efficiency.

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 void and simple types to Task, 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.name

    The name of the assembly that contains the method.

  • type

    The fully qualified class name that defines the method.

  • method

    The method name.

  • signature.return_type

    The return type of the method.

  • signature.parameter_types

    A 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.
Note: Attributes with unsupported types or invalid values are omitted.

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

The expression syntax is a domain-specific language (DSL) based on CEL that provides access to method execution context at runtime. It supports a subset of CEL with instrumentation-specific extensions.
Identifiers
Access method execution context:
Identifier Description Example
arguments Array of method arguments (zero-indexed) arguments[0]
instance The instance object (for instance methods) instance.ServiceName
return Method return value (in OnMethodEnd) return.StatusCode
method Method name method
type Declaring type name type
Member Access
Access properties and array elements:
Expression Description
arguments[0] First method argument
arguments[1] Second method argument
arguments[0].PropertyName Property of first argument
arguments[0].Nested.Property Nested property access
instance.PropertyName Property of instance object
return.ResultProperty Property 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] to arguments[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
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.
Type Truthy Condition Examples
bool The value itself true is truthy, false is falsy
string Non-null and non-empty "text" is truthy, "" is falsy
byte Non-zero 1 is truthy, 0 is falsy
sbyte Non-zero 1 is truthy, 0 is falsy
short Non-zero 1 is truthy, 0 is falsy
ushort Non-zero 1 is truthy, 0 is falsy
int Non-zero 1 is truthy, 0 is falsy
uint Non-zero 1u is truthy, 0u is falsy
long Non-zero 1L is truthy, 0L is falsy
ulong Non-zero 1UL is truthy, 0UL is falsy
float Absolute value > 0 1.23f is truthy, 0.0f is falsy
double Absolute value > 0 1.23 is truthy, 0.0 is falsy
decimal Non-zero 1.5m is truthy, 0m is falsy
null Always falsy Always treated as false
Other objects Non-null objects are truthy Custom objects are truthy if not null, falsy if null
The following example demonstrates truthy values in both attributes and status rules:
CODE
span:
  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

These examples illustrate how dynamic attributes are defined and evaluated at runtime in no‑code instrumentation. They show practical scenarios where attribute values are derived from the current request or execution context rather than being static. This helps demonstrate how to tailor telemetry data to capture meaningful, context‑specific information.
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")
Note: String comparison functions (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

Dynamic span names allow you to construct span names at runtime based on method context using expressions. This is useful for creating meaningful, contextual span names that include parameter values or other runtime information.
Important: Span names should be low-cardinality to avoid performance issues and storage overhead. Avoid including high-cardinality values like unique identifiers, timestamps, or user-specific data directly in span names. Use span attributes for high-cardinality data instead. See the

OpenTelemetry Span specification

for guidance on span name best practices.
Use the 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.
Note: Expressions for span names typically use the + 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

CODE
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
Note: Status code:
  • 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:

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

CODE
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
Important:
  • Conditions must evaluate to a boolean value.
  • Rules are processed sequentially. Only the first matching rule is applied.
  • The return value 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:

CODE
public static void TestMethodStatic();

Configuration:

CODE
instrumentation/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 kind is omitted, it defaults to internal:

CODE
public void TestMethodA();

Configuration:

CODE
instrumentation/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:

CODE
public void TestMethod(string param1, string param2);

Configuration:

CODE
instrumentation/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>:

CODE
public async Task<int> IntTaskTestMethodAsync();

Configuration:

CODE
instrumentation/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):

CODE
public static void TestMethodStatic();

Configuration with multiple attribute types:

CODE
instrumentation/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:

CODE
instrumentation/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 ValueTask or ValueTask<T>:

CODE
public async ValueTask<int> IntValueTaskTestMethodAsync();

Configuration:

CODE
instrumentation/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):

CODE
public 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>()):

CODE
instrumentation/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:

JAVA
public 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 !!0 is the first class type parameter)
  • Method-level type parameters: Use !0, !1, etc. (where !0 is the first method type parameter)
  • In the type name, use backtick notation: ClassName\N` where N is the number of generic parameters

Configuration:

CODE
instrumentation/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: internal

In 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)
Methods with return values

Instrument methods that return values:

CODE
// Method returning string
public string ReturningStringTestMethod();

// Method returning custom class
public TestClass ReturningCustomClassTestMethod();

Configuration:

CODE
instrumentation/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
Note:

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, or consumer. 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.