---
title: "Custom Instrumentation"
description: "Learn how to capture performance data on any action in your app."
url: https://docs.sentry.io/platforms/dotnet/guides/log4net/tracing/instrumentation/custom-instrumentation/
---

# Custom Instrumentation | Sentry for log4net

To capture transactions and spans customized to your organization's needs, you must first [set up tracing.](https://docs.sentry.io/platforms/dotnet/guides/log4net/tracing.md)

To instrument certain regions of your code, you can create transactions to capture them.

```csharp
// Transaction can be started by providing, at minimum, the name and the operation
var transaction = SentrySdk.StartTransaction(
  "test-transaction-name",
  "test-transaction-operation"
);

// Transactions can have child spans (and those spans can have child spans as well)
var span = transaction.StartChild("test-child-operation");

// ...
// (Perform the operation represented by the span/transaction)
// ...

span.Finish(); // Mark the span as finished
transaction.Finish(); // Mark the transaction as finished and send it to Sentry
```

For example, if you want to create a transaction for a user interaction in your application:

```csharp
// Let's say this method is invoked when a user clicks on the checkout button of your shop
public async Task PerformCheckoutAsync()
{
  // This will create a new Transaction for you
  var transaction = SentrySdk.StartTransaction(
      "checkout", // name
      "perform-checkout" // operation
  );

  // Set transaction on scope to associate with errors and get included span instrumentation
  // If there's currently an unfinished transaction, it may be dropped
  SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);

  // Validate the cart
  var validationSpan = transaction.StartChild(
      "validation", // operation
      "validating shopping cart" // description
  );

  await ValidateShoppingCartAsync();

  validationSpan.Finish();

  // Process the order
  var processSpan = transaction.StartChild(
      "process", // operation
      "processing shopping cart" // description
  )

  await ProcessShoppingCartAsync();

  processSpan.Finish();

  transaction.Finish();
}
```

This example will send a transaction `checkout` to Sentry. The transaction will contain a `validation` span that measures how long `ValidateShoppingCartAsync` took and a `process` span that measures `ProcessShoppingCartAsync`. Finally, the call to `transaction.Finish()` will finish the transaction and send it to Sentry.

## [Retrieve a Transaction](https://docs.sentry.io/platforms/dotnet/guides/log4net/tracing/instrumentation/custom-instrumentation.md#retrieve-a-transaction)

In cases where you want to attach Spans to an already ongoing Transaction you can use `SentrySdk.GetSpan()`. If there is a running Transaction or Span currently on the scope, this method will return a `SentryTransaction` or `Span`; otherwise, it returns `null`.

```csharp
var span = SentrySdk.GetSpan();

if (span == null)
{
    span = SentrySdk.StartTransaction("task", "op");
}
else
{
    span = span.StartChild("subtask");
}
```

## [Record Already-Completed Transactions](https://docs.sentry.io/platforms/dotnet/guides/log4net/tracing/instrumentation/custom-instrumentation.md#record-already-completed-transactions)

Requires Sentry SDK version `6.8.0` or higher.

`StartTransaction` measures work as it happens, using a live stopwatch. Sometimes, though, the work you want to report has already finished somewhere else — for example, spans measured on another machine or in another process and relayed to your application through a proxy. For those cases, use `SentrySdk.RecordTransaction` to record a transaction whose timing you supply explicitly:

```csharp
SentrySdk.RecordTransaction(
    "checkout",                    // name
    "http.server",                 // operation
    originalStartTimestamp,        // when the work started (DateTimeOffset)
    originalDuration,              // how long it ran (TimeSpan, must not be negative)
    traceId: originTraceId,        // optional: preserve the trace id from the originating system
    spanId: originRootSpanId,      // optional: preserve the root span id
    parentSpanId: originParentId,  // optional: continue a trace from another service
    configure: transaction =>
    {
        transaction.Release = "1.2.3";
        transaction.SetTag("origin", "proxy");

        // Child spans also take their timing up front and can be nested arbitrarily
        transaction.RecordSpan("db.query", queryStart, queryDuration, configure: span =>
        {
            span.Description = "SELECT * FROM orders";
            span.RecordSpan("db.connection", connectStart, connectDuration);
        });
    });
```

The transaction is captured and sent to Sentry once the `configure` callback returns — there is no `Finish()` to call.

Because a recorded transaction represents work that happened elsewhere, it behaves differently from one started with `StartTransaction`:

* **Timing is explicit.** Each transaction and span takes a start timestamp and a duration up front, rather than being measured live.
* **Sampling is skipped.** Recorded transactions are always sent, regardless of `TracesSampleRate` or `TracesSampler`. Your `BeforeSendTransaction` callback still runs, so you can filter or scrub there.
* **The current scope doesn't apply.** The transaction is captured against a clean scope, so breadcrumbs, user data, and tags from the current process won't leak onto work that happened elsewhere. To attach scope data that belongs with the original trace, use `transaction.ConfigureScope` inside the callback, or the `Release` and `Environment` setters.

## Pages in this section

- [Instrument Caches](https://docs.sentry.io/platforms/dotnet/guides/log4net/tracing/instrumentation/custom-instrumentation/caches-module.md)
- [Instrument Queues](https://docs.sentry.io/platforms/dotnet/guides/log4net/tracing/instrumentation/custom-instrumentation/queues-module.md)
