.NET
최종 수정: 2026. 1. 16.
.NET 계측
.NET 애플리케이션에 OpenTelemetry를 적용하는 방법을 안내합니다.
개요
.NET은 OpenTelemetry에서 잘 지원되는 플랫폼입니다.
계측 옵션
| 방식 | 설명 | 코드 변경 |
|---|---|---|
| APM Agent (eBPF) | 커널 레벨 자동 감지 | 불필요 |
| OTel .NET Agent | 자동 계측 | 불필요 |
| SDK 수동 계측 | 코드에 직접 추가 | 필요 |
자동 계측 (권장)
OTel Operator 사용
가장 쉬운 방법은 OTel Operator를 통한 자동 주입입니다.
Deployment 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-dotnet-app
namespace: production
spec:
template:
metadata:
annotations:
instrumentation.opentelemetry.io/inject-dotnet: "skuber-observability/otel-instrumentation"
spec:
containers:
- name: app
image: my-dotnet-app:1.0.0
ports:
- containerPort: 5000지원 프레임워크
자동으로 계측되는 프레임워크:
| 카테고리 | 프레임워크 |
|---|---|
| 웹 | ASP.NET Core, ASP.NET MVC |
| DB | Entity Framework Core, SqlClient, Npgsql |
| HTTP 클라이언트 | HttpClient, RestSharp |
| 메시징 | MassTransit, NServiceBus |
| gRPC | Grpc.Net.Client |
| 캐시 | StackExchange.Redis |
수동 Agent 설정
Kubernetes 외부 환경에서:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
# OTel .NET 자동 계측 에이전트 설치
ARG OTEL_VERSION=1.2.0
RUN apt-get update && apt-get install -y curl
RUN curl -sSfL https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/v${OTEL_VERSION}/otel-dotnet-auto-install.sh | bash
WORKDIR /app
COPY --from=build /app/publish .
ENV CORECLR_ENABLE_PROFILING=1
ENV CORECLR_PROFILER={918728DD-259F-4A6A-AC2B-B85E1B658318}
ENV CORECLR_PROFILER_PATH=/root/.otel-dotnet-auto/linux-x64/OpenTelemetry.AutoInstrumentation.Native.so
ENV DOTNET_ADDITIONAL_DEPS=/root/.otel-dotnet-auto/AdditionalDeps
ENV DOTNET_SHARED_STORE=/root/.otel-dotnet-auto/store
ENV DOTNET_STARTUP_HOOKS=/root/.otel-dotnet-auto/net/OpenTelemetry.AutoInstrumentation.StartupHook.dll
ENV OTEL_DOTNET_AUTO_HOME=/root/.otel-dotnet-auto
ENV OTEL_SERVICE_NAME=my-dotnet-app
ENV OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
ENTRYPOINT ["dotnet", "MyApp.dll"]수동 계측
비즈니스 로직에 커스텀 스팬을 추가하려면 수동 계측을 사용합니다.
NuGet 패키지 설치
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.HttpSDK 초기화 (Program.cs)
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using OpenTelemetry.Metrics;
var builder = WebApplication.CreateBuilder(args);
// OpenTelemetry 설정
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(
serviceName: "order-service",
serviceVersion: "1.0.0"))
.WithTracing(tracing => tracing
// 자동 계측 추가
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation()
// OTLP Exporter
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://otel-collector:4317");
}))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://otel-collector:4317");
}));
var app = builder.Build();
app.Run();트레이스 생성
ActivitySource 정의
using System.Diagnostics;
public static class Telemetry
{
public static readonly ActivitySource ActivitySource =
new ActivitySource("OrderService", "1.0.0");
}기본 스팬 생성
using System.Diagnostics;
public class OrderService
{
public async Task<Order> CreateOrderAsync(string customerId, List<Item> items)
{
// 스팬 시작
using var activity = Telemetry.ActivitySource.StartActivity("CreateOrder");
// 속성 추가
activity?.SetTag("customer.id", customerId);
activity?.SetTag("items.count", items.Count);
try
{
// 비즈니스 로직
var order = await ProcessOrderAsync(customerId, items);
// 결과 속성 추가
activity?.SetTag("order.id", order.Id);
activity?.SetTag("order.total", order.Total);
return order;
}
catch (Exception ex)
{
// 에러 기록
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
throw;
}
}
}중첩 스팬
public async Task<Order> ProcessOrderAsync(string customerId, List<Item> items)
{
using var activity = Telemetry.ActivitySource.StartActivity("ProcessOrder");
// 하위 스팬은 자동으로 부모와 연결됨
await ValidateInventoryAsync(items);
var pricing = await CalculatePricingAsync(items);
var order = await SaveOrderAsync(customerId, items, pricing);
return order;
}
private async Task ValidateInventoryAsync(List<Item> items)
{
using var activity = Telemetry.ActivitySource.StartActivity("ValidateInventory");
activity?.SetTag("items.count", items.Count);
// 재고 확인 로직
foreach (var item in items)
{
await CheckStockAsync(item);
}
}메트릭 생성
using System.Diagnostics.Metrics;
public static class OrderMetrics
{
private static readonly Meter Meter = new("OrderService", "1.0.0");
public static readonly Counter<long> OrdersCreated =
Meter.CreateCounter<long>(
"orders.created",
description: "Number of orders created");
public static readonly Histogram<double> OrderValue =
Meter.CreateHistogram<double>(
"order.value",
unit: "USD",
description: "Order value distribution");
}
// 메트릭 기록
OrderMetrics.OrdersCreated.Add(1,
new KeyValuePair<string, object?>("customer.type", "premium"));
OrderMetrics.OrderValue.Record(150.50,
new KeyValuePair<string, object?>("currency", "USD"));ASP.NET Core 통합
미들웨어 추가
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation(options =>
{
// 요청 필터링
options.Filter = (httpContext) =>
{
// 헬스체크 제외
return !httpContext.Request.Path.StartsWithSegments("/health");
};
// 요청 정보 추가
options.EnrichWithHttpRequest = (activity, httpRequest) =>
{
activity.SetTag("http.request.header.x-request-id",
httpRequest.Headers["X-Request-Id"].ToString());
};
})
.AddHttpClientInstrumentation(options =>
{
// 응답 정보 추가
options.EnrichWithHttpResponseMessage = (activity, response) =>
{
activity.SetTag("http.response.content_length",
response.Content.Headers.ContentLength);
};
}));컨트롤러에서 사용
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
[HttpPost]
public async Task<ActionResult<Order>> CreateOrder([FromBody] CreateOrderRequest request)
{
using var activity = Telemetry.ActivitySource.StartActivity("HandleOrderRequest");
activity?.SetTag("customer.id", request.CustomerId);
var order = await _orderService.CreateOrderAsync(
request.CustomerId,
request.Items);
return Ok(order);
}
}Entity Framework Core 계측
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddEntityFrameworkCoreInstrumentation(options =>
{
// SQL 쿼리 포함
options.SetDbStatementForText = true;
}));gRPC 계측
// NuGet 패키지
// dotnet add package OpenTelemetry.Instrumentation.GrpcNetClient
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddGrpcClientInstrumentation());환경 변수 설정
Kubernetes에서 환경 변수로 설정:
env:
- name: OTEL_SERVICE_NAME
value: "order-service"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-collector:4317"
- name: OTEL_TRACES_SAMPLER
value: "parentbased_traceidratio"
- name: OTEL_TRACES_SAMPLER_ARG
value: "0.1" # 10% 샘플링
- name: OTEL_DOTNET_AUTO_TRACES_ADDITIONAL_SOURCES
value: "OrderService"appsettings.json 설정
{
"OpenTelemetry": {
"ServiceName": "order-service",
"Exporter": {
"Otlp": {
"Endpoint": "http://otel-collector:4317"
}
}
}
}// 설정 사용
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
{
var config = builder.Configuration.GetSection("OpenTelemetry");
resource.AddService(config["ServiceName"]);
});비동기 코드 계측
Task 기반 비동기
public async Task<List<Order>> ProcessBatchAsync(List<OrderRequest> requests)
{
using var activity = Telemetry.ActivitySource.StartActivity("ProcessBatch");
activity?.SetTag("batch.size", requests.Count);
// 병렬 처리
var tasks = requests.Select(r => ProcessOrderAsync(r));
var results = await Task.WhenAll(tasks);
return results.ToList();
}컨텍스트 전파
public async Task CallExternalServiceAsync(Order order)
{
using var activity = Telemetry.ActivitySource.StartActivity("CallExternalService");
// HttpClient는 자동으로 컨텍스트를 전파함
var response = await _httpClient.PostAsJsonAsync(
"http://external-service/api/orders",
order);
response.EnsureSuccessStatusCode();
}트러블슈팅
트레이스가 수집되지 않음
ActivitySource 등록 확인:
.WithTracing(tracing => tracing .AddSource("OrderService") // ActivitySource 이름과 일치해야 함Exporter 연결 확인:
.AddOtlpExporter(options => { options.Endpoint = new Uri("http://otel-collector:4317"); // 디버그용 options.ExportProcessorType = ExportProcessorType.Simple; })로깅 활성화:
builder.Logging.AddOpenTelemetry(options => { options.IncludeFormattedMessage = true; options.IncludeScopes = true; });
Activity가 null
// Activity가 샘플링으로 제외될 수 있음
using var activity = Telemetry.ActivitySource.StartActivity("Operation");
// null 체크 필요
activity?.SetTag("key", "value");다음 단계
- 자동 계측 - OTel Operator 상세 설정
- 트레이스 - 트레이스 분석 방법
- 서비스 - 서비스 성능 모니터링