본문으로 건너뛰기

관측 가능성 사양

Functorium 프레임워크의 관측 가능성(Observability) 필드/태그 사양, Meter 정의 규칙, 메시지 템플릿 패턴을 정의합니다. Pipeline 실행 순서, OpenTelemetryOptions 설정, 커스텀 확장 포인트는 파이프라인 사양을 참조하십시오.

개념설명
Service Attributesservice.namespace, service.name 등 OpenTelemetry 표준 서비스 식별
Error 분류expected (비즈니스 오류), exceptional (시스템 오류), aggregate (복합 오류)
3-Pillar Field/Tagrequest.*, response.*, error.* 필드가 Logging, Metrics, Tracing에서 동일하게 사용
Meter Name{service.namespace}.{layer}[.{category}] 패턴
Instrumentrequests (Counter), responses (Counter), duration (Histogram)
Message TemplateLayer별 구조화 로그 메시지 형식과 Event ID 체계 (Application 1001-1004, Adapter 2001-2004)
Span Name{layer} {category}[.{type}] {handler}.{method}
ctx.* 컨텍스트 필드Source Generator가 Request/Response/DomainEvent 프로퍼티를 ctx.{snake_case} 필드로 자동 변환. [CtxTarget]으로 Pillar 타겟팅 (기본: Logging + Tracing, Metrics는 opt-in)

Functorium은 서비스 식별을 위해 OpenTelemetry Service Attributes를 사용합니다.

Attribute설명예시
service.namespaceservice.name의 네임스페이스. 서비스 그룹을 구분하는 데 도움이 됩니다(예: 팀별 또는 환경별).mycompany.production
service.name서비스의 논리적 이름. 수평 확장된 모든 인스턴스에서 동일해야 합니다.orderservice
service.version서비스 API 또는 구현의 버전 문자열.2.0.0
service.instance.id서비스 인스턴스의 고유 ID. service.namespace,service.name 쌍당 전역적으로 고유해야 합니다. 가능한 경우 HOSTNAME 환경 변수를 사용하고, 그렇지 않으면 Environment.MachineName으로 대체됩니다.my-pod-abc123 (Kubernetes), DESKTOP-ABC123 (Windows)
deployment.environment배포 환경을 식별하는 속성.production, staging

권장: service.nameservice.namespace에는 소문자 값을 사용하세요(예: mycompany.production, orderservice). 이렇게 하면 OpenTelemetry 규칙과의 일관성을 보장하고 다운스트림 시스템(대시보드, 쿼리, 알림)에서 대소문자 구분 문제를 방지할 수 있습니다.

다음 표는 에러 원인에 따라 error.typeerror.code 태그 값이 어떻게 결정되는지 정리합니다.

Error Caseerror.typeerror.code설명
IHasErrorCode + IsExpected"expected"오류 코드오류 코드가 있는 예상 비즈니스 로직 오류
IHasErrorCode + IsExceptional"exceptional"오류 코드오류 코드가 있는 예외 시스템 오류
ManyErrors"aggregate"Primary 오류 코드여러 오류가 집계됨(Exceptional이 우선)
Expected (LanguageExt)"expected"타입 이름오류 코드가 없는 LanguageExt 기본 예상 오류
Exceptional (LanguageExt)"exceptional"타입 이름오류 코드가 없는 LanguageExt 기본 예외 오류

error.type@error.ErrorType은 서로 다른 목적을 위해 다른 값 형식을 사용합니다.

Error Typeerror.type (필터링용)@error.ErrorType (상세용)
Expected Error"expected""ErrorCodeExpected"
Exceptional Error"exceptional""ErrorCodeExceptional"
Aggregate Error"aggregate""ManyErrors"
LanguageExt Expected"expected""Expected"
LanguageExt Exceptional"exceptional""Exceptional"
  • error.type: 로그 필터링/쿼리를 위한 표준화된 값(Metrics/Tracing과 일관됨)
  • @error.ErrorType: 상세한 오류 타입 식별을 위한 실제 클래스 이름

OpenTelemetry 시맨틱 규칙을 준수하여 snake_case + dot 표기법을 사용합니다.

# 올바른 예시
request.layer
request.category.type
request.handler.method
response.status
error.code
# 잘못된 예시
requestLayer # camelCase 사용 금지
request-layer # kebab-case 사용 금지
REQUEST_LAYER # UPPER_SNAKE_CASE 사용 금지

필드는 네임스페이스와 속성의 계층 구조로 구성합니다.

네임스페이스설명예시
request.*요청 관련 정보request.layer, request.handler.name
response.*응답 관련 정보response.status, response.elapsed
error.*오류 관련 정보error.type, error.code
구분규칙예시
정적 필드 (이벤트 관련).countrequest.event.count
동적 필드 (파라미터 크기)_countrequest.params.orders_count
형용사/명사 조합_countresponse.event.success_count, response.event.failure_count
# 올바른 예시
request.event.count # 정적 .count
response.event.success_count # 형용사 조합 _count
request.params.orders_count # 동적 파라미터 _count
# 잘못된 예시
response.event.success.count # 조합 count에 .count 사용 금지
request.params.orders.count # 동적 필드에 .count 사용 금지

Application 레이어: (단위 테스트: Logging, Metrics, Tracing)

Field/TagLoggingMetricsTracing설명
request.layer아키텍처 레이어 ("application")
request.category.name요청 카테고리 ("usecase")
request.category.typeCQRS 타입 ("command", "query")
request.handler.namehandler 클래스 이름
request.handler.methodhandler 메서드 이름 ("Handle")
response.status응답 상태 ("success", "failure")
response.elapsed-*경과 시간(초)
error.type오류 분류 ("expected", "exceptional", "aggregate")
error.code도메인 특화 오류 코드
@error--구조화된 오류 객체(상세)

Adapter 레이어: (단위 테스트: Logging, Metrics, Tracing)

Field/TagLoggingMetricsTracing설명
request.layer아키텍처 레이어 ("adapter")
request.category.name카테고리 (예: "repository")
request.handler.namehandler 클래스 이름
request.handler.methodhandler 메서드 이름
response.status응답 상태 ("success", "failure")
response.elapsed-*경과 시간(초)
error.type오류 분류 ("expected", "exceptional", "aggregate")
error.code도메인 특화 오류 코드
@error--구조화된 오류 객체(상세)

* response.elapsed가 Metrics 태그가 아닌 이유:

  • Metrics는 처리 시간을 캡처하기 위해 전용 duration Histogram instrument를 사용하며, 이는 지연 시간 측정에 대한 OpenTelemetry 권장 접근 방식입니다.
  • 경과 시간을 태그로 사용하면 높은 카디널리티 폭발을 유발합니다(각 고유한 duration 값이 새로운 시계열을 생성하여 메트릭 저장소 및 쿼리 성능이 저하됨).
  • Histogram은 개별 경과 값보다 모니터링에 더 유용한 통계적 집계(백분위수, 평균, 카운트)를 제공합니다.

(단위 테스트: Logging, Metrics, Tracing)

DomainEvent Publisher는 Adapter 레이어로 분류되며, request.layer"adapter", request.category.name"event"입니다.

Field/TagLoggingMetricsTracing설명
request.layer아키텍처 레이어 ("adapter")
request.category.name요청 카테고리 ("event")
request.handler.nameEvent 타입명 또는 Aggregate 타입명
request.handler.method메서드 이름 ("Publish", "PublishTrackedEvents")
request.aggregate.count--Aggregate 유형 수 (PublishTrackedEvents 전용)
request.event.count-배치 발행 시 이벤트 개수 (Aggregate 전용)
response.status응답 상태 ("success", "failure")
response.elapsed-*경과 시간(초)
response.event.success_count-부분 실패 시 성공한 이벤트 수 (Partial Failure 전용)
response.event.failure_count-부분 실패 시 실패한 이벤트 수 (Partial Failure 전용)
error.type오류 분류 ("expected", "exceptional")
error.code도메인 특화 오류 코드
@error--구조화된 오류 객체(상세)

(단위 테스트: Logging, Metrics, Tracing)

DomainEventHandler는 Application 레이어로 분류되며, request.layer"application", request.category.name"usecase", request.category.type"event"입니다.

Field/TagLoggingMetricsTracing설명
request.layer아키텍처 레이어 ("application")
request.category.name요청 카테고리 ("usecase")
request.category.typeCQRS 타입 ("event")
request.handler.namehandler 클래스 이름
request.handler.method메서드 이름 ("Handle")
request.event.type-이벤트 타입명
request.event.id-이벤트 고유 ID
@request.message--이벤트 객체 (요청 시)
response.status응답 상태 ("success", "failure")
response.elapsed-*-경과 시간(초)
error.type오류 분류 ("expected", "exceptional")
error.code도메인 특화 오류 코드

Note: DomainEventHandler의 response.elapsed는 Tracing Span 태그에 설정되지 않습니다 (Logging 전용). Span은 자체적으로 시작/종료 시간(duration)을 가지므로 별도의 elapsed 필드는 중복입니다. DomainEventHandler의 ErrorResponse는 Exception 객체가 직접 로깅됩니다 (@error 대신). DomainEventHandler는 ValueTask 반환이므로 @response.message를 기록하지 않습니다.


ctx.* 사용자 정의 컨텍스트 필드 (3-Pillar)

섹션 제목: “ctx.* 사용자 정의 컨텍스트 필드 (3-Pillar)”

ctx.* 필드는 비즈니스 맥락을 Logging, Tracing, Metrics에 동시 전파하는 사용자 정의 컨텍스트 필드입니다. Source Generator가 Request/Response/DomainEvent의 공개 프로퍼티를 자동 감지하여 IUsecaseCtxEnricher<TRequest, TResponse> 또는 IDomainEventCtxEnricher<TEvent> 구현체를 생성합니다. CtxEnricherPipeline이 최선두 Pipeline으로 실행되어 후속 Metrics/Tracing/Logging Pipeline에서 ctx.* 데이터에 접근 가능합니다.

항목설명
대상 PillarLogging + Tracing (기본값). Metrics는 [CtxTarget]으로 명시적 opt-in
생성기 (Usecase)CtxEnricherGeneratorICommandRequest<T> / IQueryRequest<T> 구현 record 감지
생성기 (DomainEvent)DomainEventCtxEnricherGeneratorIDomainEventHandler<T> 구현 클래스에서 T 감지
런타임 메커니즘CtxEnricherContext.Push(name, value, pillars) — Logging/Tracing/Metrics 동시 전파
대상 프로퍼티공개 스칼라 및 컬렉션 프로퍼티 (복합 타입은 제외)
파이프라인 순서CtxEnricher → Metrics → Tracing → Logging → Validation → ... → Handler
Pillar메커니즘설명
LoggingSerilog LogContext.PushProperty구조화 로그 필드로 출력
TracingActivity.Current?.SetTagSpan Attribute로 출력
MetricsTagMetricsTagContext (AsyncLocal) → TagList 병합기존 Counter/Histogram의 차원으로 추가
MetricsValue별도 Histogram instrument에 값 기록수치 필드를 통계적 집계 대상으로 기록
[Flags]
public enum CtxPillar
{
Logging = 1, // Serilog LogContext
Tracing = 2, // Activity.SetTag
MetricsTag = 4, // TagList 차원 (저카디널리티 전용)
MetricsValue = 8, // Histogram 값 기록 (수치 전용)
Default = Logging | Tracing, // 기본값
All = Logging | Tracing | MetricsTag,
}
범위패턴예시 (C# → ctx 필드)
Usecase Requestctx.{containing_type}.request.{property}PlaceOrderCommand.Request.CustomerIdctx.place_order_command.request.customer_id
Usecase Responsectx.{containing_type}.response.{property}PlaceOrderCommand.Response.OrderIdctx.place_order_command.response.order_id
DomainEvent (최상위)ctx.{event}.{property}OrderPlacedEvent.CustomerIdctx.order_placed_event.customer_id
DomainEvent (중첩)ctx.{containing_type}.{event}.{property}Order.CreatedEvent.OrderIdctx.order.created_event.order_id
인터페이스 범위ctx.{interface (I 제거)}.{property}IRegional.RegionCodectx.regional.region_code
[CtxRoot] 승격ctx.{property}[CtxRoot] CustomerIdctx.customer_id
컬렉션{위 규칙}_count 접미사List<OrderLine> Linesctx.place_order_command.request.lines_count

모든 이름은 PascalCase → snake_case로 변환됩니다. 인터페이스 이름의 I 접두사는 제거됩니다 (ICustomerRequestcustomer_request, IXx). 필드 이름은 모든 Pillar에서 동일합니다 — Logging, Tracing, Metrics에서 같은 ctx.* 이름을 사용합니다.

[CtxRoot]
[CtxTarget(CtxPillar.All)] // 모든 프로퍼티 → 3-Pillar Tag
public interface IRegional
{
string RegionCode { get; } // → ctx.region_code (Root + MetricsTag)
}
public sealed class PlaceOrderCommand
{
public sealed record Request(
string CustomerId, // 기본(L+T). 고카디널리티
[CtxTarget(CtxPillar.All)] bool IsExpress, // 3-Pillar Tag. boolean 안전
[CtxTarget(CtxPillar.Default | CtxPillar.MetricsValue)]
int ItemCount, // L+T + Histogram 기록
List<OrderLine> Lines, // 기본(L+T). count 전파
[CtxTarget(CtxPillar.Logging)] string InternalNote, // Logging 전용
[CtxIgnore] string DebugInfo // 완전 제외
) : ICommandRequest<Response>, IRegional;
public sealed record Response(
string OrderId,
[CtxTarget(CtxPillar.Default | CtxPillar.MetricsValue)]
decimal TotalAmount // L+T + Histogram 기록
);
}

✅ = 지원, - = 미지원/해당없음

ctx 필드타입LoggingTracingMetrics TagMetrics Value
ctx.region_codekeyword-
ctx.place_order_command.request.customer_idkeyword--
ctx.place_order_command.request.is_expressboolean-
ctx.place_order_command.request.item_countlong-
ctx.place_order_command.request.lines_countlong--
ctx.place_order_command.request.internal_notekeyword---
ctx.place_order_command.response.order_idkeyword--
ctx.place_order_command.response.total_amountdouble-
C# 타입OpenSearch 타입 그룹비고
boolboolean
byte, sbyte, short, ushort, int, uint, long, ulonglong
float, double, decimaldouble
stringkeyword
Guid, DateTime, DateTimeOffset, TimeSpan, DateOnly, TimeOnly, Urikeyword
enumkeyword
Option<T> (LanguageExt)keyword
IValueObject, IEntityId<T> 구현체keyword.ToString() 호출 (DomainEvent 전용)
Nullable<T>내부 T에 위임
컬렉션 (List<T>, IReadOnlyList<T> 등)long요소 count 값
기타 복합 타입 (class, record, struct)— (제외)스칼라/컬렉션만 대상

동일한 ctx 필드명에 서로 다른 타입 그룹이 할당되면 OpenSearch 동적 매핑 충돌이 발생합니다 (컴파일 타임 진단 FUNCTORIUM002).

어트리뷰트적용 대상효과
[CtxRoot]interface, property, parameter필드를 ctx.{field} 루트 레벨로 승격. Pillar 타겟팅과 독립 (네이밍에만 영향)
[CtxIgnore]class, property, parameter모든 Pillar에서 제외. [CtxTarget]보다 우선
[CtxTarget(CtxPillar)]interface, property, parameterPillar 타겟 지정. 미지정 시 Default (Logging + Tracing)
[CtxIgnore]? → YES → 모든 Pillar 제외
↓ NO
[CtxTarget] 지정됨? → YES → 지정된 Pillar
↓ NO
기본값 → CtxPillar.Default (Logging + Tracing)

프로퍼티/파라미터 수준 설정이 인터페이스 수준 설정보다 항상 우선합니다.

카디널리티 수준해당 타입MetricsTagMetricsValue
Fixedbool안전불가
BoundedLowenum조건부불가
Unboundedstring, Guid, DateTime, IValueObject, IEntityId<T>, Option<T>경고 (FUNCTORIUM005)불가
Numericint, long, decimal, double경고 (FUNCTORIUM005)허용
NumericCount컬렉션 count경고 (FUNCTORIUM005)허용

DomainEvent Enricher는 IDomainEvent 인터페이스의 기본 속성을 자동으로 제외합니다. 이 속성들은 이미 표준 필드(request.event.id 등)로 출력됩니다.

제외 속성이유
OccurredAt타임스탬프는 @timestamp로 별도 출력
EventIdrequest.event.id로 별도 출력
CorrelationId표준 상관 ID 필드로 별도 관리
CausationId표준 인과 ID 필드로 별도 관리

ctx. 접두사 없이 LogContext.PushProperty로 푸시된 PascalCase 프로퍼티는 OpenSearchJsonFormatter가 자동으로 ctx.snake_case로 변환합니다.

CustomerId → ctx.customer_id
OrderLineCount → ctx.order_line_count

Source Generator가 생성한 코드는 이미 ctx. 접두사를 포함하므로 안전망 변환 대상이 아닙니다. 이 변환은 수동으로 LogContext.PushProperty를 호출한 경우에만 적용됩니다.

생성된 Enricher 클래스는 partial class이며, 다음 확장 포인트를 제공합니다.

Enricher 유형Partial Method호출 시점
UsecaseOnEnrichRequest(request, disposables)Request 처리 시작 시
UsecaseOnEnrichResponse(request, response, disposables)Response 처리 완료 시
DomainEventOnEnrich(domainEvent, disposables)Handler 실행 전
Enricher 유형Helper Method생성되는 필드 패턴
UsecasePushRequestCtx(disposables, fieldName, value, pillars)ctx.{type}.request.{fieldName}
UsecasePushResponseCtx(disposables, fieldName, value, pillars)ctx.{type}.response.{fieldName}
DomainEventPushEventCtx(disposables, fieldName, value, pillars)ctx.{event}.{fieldName}
공통PushRootCtx(disposables, fieldName, value, pillars)ctx.{fieldName}[CtxRoot] 속성 존재 시에만 생성
진단 코드심각도조건설명
FUNCTORIUM002Warning동일 ctx 필드명에 서로 다른 OpenSearch 타입 그룹 할당예: ctx.customer_id가 enricher A에서는 keyword, enricher B에서는 long
FUNCTORIUM003WarningRequest 타입이 private/protected 접근 제한[CtxIgnore]를 Request record에 적용하여 경고 억제
FUNCTORIUM004WarningEvent 타입이 private/protected 접근 제한[CtxIgnore]를 Event record에 적용하여 경고 억제
FUNCTORIUM005Warning고카디널리티 타입 + MetricsTagstring/Guid/수치를 MetricsTag로 지정 시 카디널리티 폭발 경고
FUNCTORIUM006Error비수치 타입 + MetricsValueboolean/keywordMetricsValue로 지정 시 에러
FUNCTORIUM007WarningMetricsTag + MetricsValue 동시 지정동일 프로퍼티에 Tag와 Value 동시 지정 시 경고
통합 지점호출 위치설명
CtxEnricherPipelineApplication 레이어 (최선두)IUsecaseCtxEnricher<TRequest, TResponse> DI 주입. EnrichRequest/EnrichResponse 호출로 3-Pillar 동시 전파
ObservableDomainEventNotificationPublisherDomainEvent HandlerIDomainEventCtxEnricher<TEvent>를 런타임 해석 후 Enrich 호출
UsecaseMetricsPipelineApplication 레이어MetricsTagContext에서 ctx.* MetricsTag를 읽어 기존 TagList에 병합
LogTestContext테스트enrichFromLogContext: true 옵션으로 ctx.* 필드 캡처 및 검증

Field NameApplication 레이어Adapter 레이어설명
Static Fields
request.layer"application""adapter"요청 레이어 식별자
request.category.name"usecase"카테고리 이름요청 카테고리 식별자
request.category.type"command" / "query"-CQRS 타입
request.handler.namehandler 이름handler 이름handler 클래스 이름
request.handler.method"Handle"메서드 이름handler 메서드 이름
response.status"success" / "failure""success" / "failure"응답 상태
response.elapsed경과 시간(초)경과 시간(초)경과 시간(초)
error.type"expected" / "exceptional" / "aggregate""expected" / "exceptional" / "aggregate"오류 분류
error.code오류 코드오류 코드도메인 특화 오류 코드
@error오류 객체(구조화)오류 객체(구조화)오류 데이터(상세)
Dynamic Fields
@request.message전체 Command/Query 객체전체 파라미터 객체 (Debug)요청 메시지
@response.message전체 응답 객체메서드 반환 값 (Debug)응답 메시지
@request.params-type-filtered 파라미터 복합 객체 (Info/Debug)요청 파라미터
EventLog 수준Application 레이어Adapter 레이어설명
RequestInformation1001 application.request2001 adapter.request요청 수신
Request (Debug)Debug-2001 adapter.request파라미터 값이 포함된 요청
Response SuccessInformation1002 application.response.success2002 adapter.response.success성공 응답
Response Success (Debug)Debug-2002 adapter.response.success결과 값이 포함된 응답
Response WarningWarning1003 application.response.warning2003 adapter.response.warning예상 오류(비즈니스 로직)
Response ErrorError1004 application.response.error2004 adapter.response.error예외 오류(시스템 장애)
# Request
{request.layer} {request.category.name}.{request.category.type} {request.handler.name}.{request.handler.method} requesting with {@request.message}
# Response - Success
{request.layer} {request.category.name}.{request.category.type} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {@response.message}
# Response - Warning/Error
{request.layer} {request.category.name}.{request.category.type} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {error.type}:{error.code} {@error}
# Request (Information) - 5 params
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} {@request.params} requesting
# Request (Debug) - 6 params
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} {@request.params} requesting with {@request.message}
# Response (Information) - 6 params
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s
# Response (Debug) - 7 params
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {@response.message}
# Response Warning/Error
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {error.type}:{error.code} {@error}
레이어방식테스트참고
Application직접 ILogger.LogXxx() 호출UsecaseLoggingPipelineStructureTests7개 이상의 파라미터가 LoggerMessage.Define의 6개 제한을 초과
AdapterLoggerMessage.Define 델리게이트ObservableObservableSignalgingStructureTests제로 할당, 고성능

Application Usecase vs DomainEvent Publisher vs DomainEventHandler 필드 비교:

✅ = 지원, ✅ (조건) = 조건부 지원, - = 미지원/해당없음

FieldApplication UsecaseDomainEvent PublisherDomainEventHandler
request.layer"application""adapter""application"
request.category.name"usecase""event""usecase"
request.category.type"command" / "query"-"event"
request.handler.namehandler 클래스명Event/Aggregate 타입명handler 클래스명
request.handler.method"Handle""Publish" / "PublishTrackedEvents""Handle"
@request.messageCommand/Query 객체이벤트 객체이벤트 객체
@response.message응답 객체--
request.event.count-✅ (Aggregate만)-
response.event.success_count-✅ (Partial Failure만)-
response.event.failure_count-✅ (Partial Failure만)-
response.status"success" / "failure""success" / "failure""success" / "failure"
response.elapsed경과 시간(초)경과 시간(초)경과 시간(초)
error.type"expected" / "exceptional" / "aggregate""expected" / "exceptional""expected" / "exceptional"
error.code오류 코드오류 코드오류 코드
@error오류 객체오류 객체오류 객체 (Exception)

Error 분류 상세는 Error 분류 섹션 참조.

# Request - 단일 이벤트
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} requesting with {@request.message}
# Request - Aggregate 다중 이벤트
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} requesting with {request.event.count} events
# Response - Success
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s
# Response - Success (Aggregate)
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {request.event.count} events
# Response - Warning/Error
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {error.type}:{error.code} {@error}
# Response - Warning/Error (Aggregate)
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {request.event.count} events with {error.type}:{error.code} {@error}
# Response - Partial Failure (Aggregate)
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} responded {response.status} in {response.elapsed:0.0000} s with {request.event.count} events partial failure: {response.event.success_count} succeeded, {response.event.failure_count} failed

DomainEvent Publisher는 Adapter 레이어로 분류되므로, Adapter 레이어와 동일한 Event ID를 사용합니다.

EventIDName
Request2001adapter.request
Success2002adapter.response.success
Warning2003adapter.response.warning
Error2004adapter.response.error

DomainEventHandler는 Publisher가 발행한 이벤트를 처리하는 Handler 관점의 로깅입니다. request.layer"application", request.category.name"usecase", request.category.type"event"입니다.

# Request
{request.layer} {request.category.name}.{request.category.type} {request.handler.name}.{request.handler.method} {request.event.type} {request.event.id} requesting with {@request.message}
# Response - Success
{request.layer} {request.category.name}.{request.category.type} {request.handler.name}.{request.handler.method} {request.event.type} {request.event.id} responded {response.status} in {response.elapsed:0.0000} s
# Response - Warning/Error
{request.layer} {request.category.name}.{request.category.type} {request.handler.name}.{request.handler.method} {request.event.type} {request.event.id} responded {response.status} in {response.elapsed:0.0000} s with {error.type}:{error.code} {@error}

DomainEventHandler는 Application 레이어의 usecase로 분류되므로, Application 레이어와 동일한 Event ID를 사용합니다.

EventIDName
Request1001application.request
Success1002application.response.success
Warning1003application.response.warning
Error1004application.response.error
레이어방식테스트참고
DomainEvent PublisherDecoratorDomainEventPublisherLoggingStructureTestsAdapter 레이어 패턴
DomainEvent HandlerINotificationPublisherDomainEventHandlerLoggingStructureTestsApplication 레이어 패턴
DomainEvent Handler EnricherIDomainEventCtxEnricher<TEvent>DomainEventHandlerEnricherLoggingStructureTestsCtxEnricherContext 기반 3-Pillar Enrichment

레이어Meter Name 패턴예시 (ServiceNamespace = "mycompany.production")
Application{service.namespace}.applicationmycompany.production.application
Adapter{service.namespace}.adapter.{category}mycompany.production.adapter.repository
DomainEvent Publisher{service.namespace}.adapter.eventmycompany.production.adapter.event
DomainEvent Handler{service.namespace}.applicationmycompany.production.application
InstrumentApplication 레이어Adapter 레이어DomainEvent PublisherDomainEvent HandlerTypeUnit
requestsapplication.usecase.{type}.requestsadapter.{category}.requestsadapter.event.requestsapplication.usecase.event.requestsCounter{request}
responsesapplication.usecase.{type}.responsesadapter.{category}.responsesadapter.event.responsesapplication.usecase.event.responsesCounter{response}
durationapplication.usecase.{type}.durationadapter.{category}.durationadapter.event.durationapplication.usecase.event.durationHistograms
Tag KeyrequestCounterdurationHistogramresponseCounter (success)responseCounter (failure)
request.layer"application""application""application""application"
request.category.name"usecase""usecase""usecase""usecase"
request.category.type"command" / "query""command" / "query""command" / "query""command" / "query"
request.handler.namehandler 이름handler 이름handler 이름handler 이름
request.handler.method"Handle""Handle""Handle""Handle"
response.status--"success""failure"
error.type---"expected" / "exceptional" / "aggregate"
error.code---Primary 오류 코드
Total Tags5568
Tag KeyrequestCounterdurationHistogramresponseCounter (success)responseCounter (failure)
request.layer"adapter""adapter""adapter""adapter"
request.category.name카테고리 이름카테고리 이름카테고리 이름카테고리 이름
request.handler.namehandler 이름handler 이름handler 이름handler 이름
request.handler.method메서드 이름메서드 이름메서드 이름메서드 이름
response.status--"success""failure"
error.type---"expected" / "exceptional" / "aggregate"
error.code---오류 코드
Total Tags4457

Error 분류 상세는 Error 분류 섹션 참조.

Tag KeyrequestCounterdurationHistogramresponseCounter (success)responseCounter (failure)
request.layer"adapter""adapter""adapter""adapter"
request.category.name"event""event""event""event"
request.handler.namehandler 이름handler 이름handler 이름handler 이름
request.handler.method메서드 이름메서드 이름메서드 이름메서드 이름
response.status--"success""failure"
error.type---"expected" / "exceptional"
error.code---오류 코드
Total Tags4457

DomainEvent Metrics에서 제외되는 태그: request.event.count, response.event.success_count, response.event.failure_count는 Metrics 태그로 사용하지 않습니다. 이 값들은 각각 고유한 수치를 가지므로 태그로 사용하면 높은 카디널리티 폭발을 유발합니다. 이는 response.elapsed를 Metrics 태그로 사용하지 않는 것과 동일한 원칙입니다.

Tag KeyrequestCounterdurationHistogramresponseCounter (success)responseCounter (failure)
request.layer"application""application""application""application"
request.category.name"usecase""usecase""usecase""usecase"
request.category.type"event""event""event""event"
request.handler.namehandler 이름handler 이름handler 이름handler 이름
request.handler.method"Handle""Handle""Handle""Handle"
response.status--"success""failure"
error.type---"expected" / "exceptional"
error.code---오류 코드
Total Tags5568
레이어방식테스트참고
ApplicationIPipelineBehavior + IMeterFactoryUsecaseMetricsPipelineStructureTestsMediator pipeline
AdapterSource GeneratorObservablePortMetricsStructureTests자동 생성된 metrics instruments
DomainEvent PublisherDecorator + IMeterFactoryDomainEventPublisherMetricsStructureTestsAdapter 레이어 패턴
DomainEvent HandlerINotificationPublisher + IMeterFactoryDomainEventHandlerMetricsStructureTestsApplication 레이어 패턴

PropertyApplication 레이어Adapter 레이어
Span Name{layer} {category}.{type} {handler}.{method}{layer} {category} {handler}.{method}
Exampleapplication usecase.command CreateOrderCommandHandler.Handleadapter repository OrderRepository.GetById
KindInternalInternal

Span Name 형식 차이: Application 레이어는 .{type} 세그먼트(command/query/event)를 포함하지만, Adapter 레이어는 CQRS 타입 구분이 없으므로 .{type} 세그먼트가 생략됩니다.

Tag KeyApplication 레이어Adapter 레이어설명
Request Tags
request.layer"application""adapter"레이어 식별자
request.category.name"usecase"카테고리 이름카테고리 식별자
request.category.type"command" / "query"-CQRS 타입
request.handler.namehandler 이름handler 이름handler 클래스 이름
request.handler.method"Handle"메서드 이름메서드 이름
Response Tags
response.status"success" / "failure""success" / "failure"응답 상태
response.elapsed경과 시간(초)경과 시간(초)경과 시간(초)
Error Tags
error.type"expected" / "exceptional" / "aggregate""expected" / "exceptional" / "aggregate"오류 분류
error.code오류 코드오류 코드오류 코드
ActivityStatusOk / ErrorOk / ErrorOpenTelemetry 상태

Error 분류 상세는 Error 분류 섹션 참조.

PropertyPublishPublishTrackedEvents
Span Nameadapter event {EventType}.Publishadapter event PublishTrackedEvents.PublishTrackedEvents
KindInternalInternal
Tag KeyRequestSuccess ResponseFailure Response
request.layer"adapter""adapter""adapter"
request.category.name"event""event""event"
request.handler.nameevent type nameevent type nameevent type name
request.handler.method"Publish""Publish""Publish"
response.elapsed-경과 시간(초)경과 시간(초)
response.status-"success""failure"
error.type--"expected" / "exceptional"
error.code--오류 코드
Total Tags468

Publisher Tag 구조 (PublishTrackedEvents)

섹션 제목: “Publisher Tag 구조 (PublishTrackedEvents)”
Tag KeyRequestSuccessPartial FailureTotal Failure
request.layer"adapter""adapter""adapter""adapter"
request.category.name"event""event""event""event"
request.handler.name"PublishTrackedEvents""PublishTrackedEvents""PublishTrackedEvents""PublishTrackedEvents"
request.handler.method"PublishTrackedEvents""PublishTrackedEvents""PublishTrackedEvents""PublishTrackedEvents"
request.aggregate.countaggregate countaggregate countaggregate countaggregate count
request.event.countevent countevent countevent countevent count
response.elapsed-경과 시간(초)경과 시간(초)경과 시간(초)
response.status-"success""failure""failure"
response.event.success_count--success count-
response.event.failure_count--failure count-
error.type---"expected" / "exceptional"
error.code---오류 코드
Total Tags681010
Property설명
Span Nameapplication usecase.event {HandlerName}.Handle
KindInternal
Tag KeySuccessFailure
request.layer"application""application"
request.category.name"usecase""usecase"
request.category.type"event""event"
request.handler.namehandler namehandler name
request.handler.method"Handle""Handle"
request.event.typeevent type nameevent type name
request.event.idevent idevent id
response.status"success""failure"
error.type-"expected" / "exceptional"
error.code-오류 코드
Total Tags810

Note: Handler의 response.elapsed는 Activity 태그에 설정되지 않습니다 (Logging 전용).

레이어방식테스트참고
ApplicationIPipelineBehavior + ActivitySource.StartActivity()UsecaseTracingPipelineStructureTestsMediator pipeline
AdapterSource GeneratorObservablePortTracingStructureTests자동 생성된 Activity spans
DomainEvent PublisherDecorator + ActivitySource.StartActivity()DomainEventPublisherTracingStructureTestsAdapter 레이어 패턴
DomainEvent HandlerINotificationPublisher + ActivitySource.StartActivity()DomainEventHandlerTracingStructureTestsApplication 레이어 패턴

구성 요소파일 경로
필드 이름 생성 헬퍼Src/Functorium.SourceGenerators/Generators/ObservablePortGenerator/CollectionTypeHelper.cs
Application LoggingSrc/Functorium.Adapters/Observabilities/Pipelines/UsecaseLoggingPipeline.cs
Adapter LoggingSource Generator 생성 코드
Application MetricsSrc/Functorium.Adapters/Observabilities/Pipelines/UsecaseMetricsPipeline.cs
Application TracingSrc/Functorium.Adapters/Observabilities/Pipelines/UsecaseTracingPipeline.cs
DomainEvent PublisherSrc/Functorium.Adapters/Observabilities/Events/ObservableDomainEventPublisher.cs
Custom Pipeline 마커Src/Functorium.Adapters/Observabilities/Pipelines/ICustomUsecasePipeline.cs
Ctx Enricher PipelineSrc/Functorium.Adapters/Observabilities/Pipelines/CtxEnricherPipeline.cs
Ctx Enricher 인터페이스Src/Functorium/Applications/Observabilities/IUsecaseCtxEnricher.cs
DomainEvent Ctx Enricher 인터페이스Src/Functorium/Applications/Observabilities/IDomainEventCtxEnricher.cs
CtxEnricher Source GeneratorSrc/Functorium.SourceGenerators/Generators/CtxEnricherGenerator/CtxEnricherGenerator.cs
DomainEvent CtxEnricher Source GeneratorSrc/Functorium.SourceGenerators/Generators/DomainEventCtxEnricherGenerator/DomainEventCtxEnricherGenerator.cs
CtxEnricherContextSrc/Functorium/Applications/Observabilities/CtxEnricherContext.cs
MetricsTagContextSrc/Functorium.Adapters/Observabilities/Contexts/MetricsTagContext.cs
CtxPillar enumSrc/Functorium/Applications/Observabilities/CtxPillar.cs
CtxRoot 어트리뷰트Src/Functorium/Applications/Observabilities/CtxRootAttribute.cs
CtxIgnore 어트리뷰트Src/Functorium/Applications/Observabilities/CtxIgnoreAttribute.cs
CtxTarget 어트리뷰트Src/Functorium/Applications/Observabilities/CtxTargetAttribute.cs
Tracing Custom BaseSrc/Functorium.Adapters/Observabilities/Pipelines/UsecaseTracingCustomPipelineBase.cs
Metric Custom BaseSrc/Functorium.Adapters/Observabilities/Pipelines/UsecaseMetricCustomPipelineBase.cs
Pipeline 설정Src/Functorium.Adapters/Observabilities/Builders/Configurators/PipelineConfigurator.cs
테스트파일 경로
Application Logging 구조Tests/Functorium.Tests.Unit/AdaptersTests/Observabilities/Pipelines/UsecaseLoggingPipelineStructureTests.cs
Adapter Logging 구조Tests/Functorium.Tests.Unit/AdaptersTests/SourceGenerators/ObservableObservableSignalgingStructureTests.cs
Application Metrics 구조Tests/Functorium.Tests.Unit/AdaptersTests/Observabilities/Pipelines/UsecaseMetricsPipelineStructureTests.cs
Adapter Metrics 구조Tests/Functorium.Tests.Unit/AdaptersTests/SourceGenerators/ObservablePortMetricsStructureTests.cs
Application Tracing 구조Tests/Functorium.Tests.Unit/AdaptersTests/Observabilities/Pipelines/UsecaseTracingPipelineStructureTests.cs
Adapter Tracing 구조Tests/Functorium.Tests.Unit/AdaptersTests/SourceGenerators/ObservablePortTracingStructureTests.cs
DomainEvent Publisher LoggingTests/Functorium.Tests.Unit/AdaptersTests/Observabilities/Events/DomainEventPublisherLoggingStructureTests.cs
DomainEvent Handler LoggingTests/Functorium.Tests.Unit/AdaptersTests/Observabilities/Events/DomainEventHandlerLoggingStructureTests.cs
CtxEnricher Source GeneratorTests/Functorium.Tests.Unit/AdaptersTests/SourceGenerators/CtxEnricherGeneratorTests.cs
DomainEvent CtxEnricher Source GeneratorTests/Functorium.Tests.Unit/AdaptersTests/SourceGenerators/DomainEventCtxEnricherGeneratorTests.cs
Ctx Enricher 통합Tests/Functorium.Tests.Unit/AdaptersTests/Observabilities/Pipelines/UsecaseLoggingPipelineEnricherTests.cs
DomainEvent Handler Enricher LoggingTests/Functorium.Tests.Unit/AdaptersTests/Observabilities/Events/DomainEventHandlerEnricherLoggingStructureTests.cs
DomainEvent Handler Enricher MetricsTests/Functorium.Tests.Unit/AdaptersTests/Observabilities/Events/DomainEventHandlerMetricsStructureTests.cs
DomainEvent Handler Enricher TracingTests/Functorium.Tests.Unit/AdaptersTests/Observabilities/Events/DomainEventHandlerTracingStructureTests.cs
Tracing Custom BaseTests/Functorium.Tests.Unit/AdaptersTests/Observabilities/Pipelines/UsecaseTracingCustomPipelineBaseTests.cs
Pipeline 설정Tests/Functorium.Tests.Unit/AdaptersTests/Observabilities/Configurators/PipelineConfiguratorTests.cs

ObservableSignal — Adapter 구현 내부 개발자 로깅 API

섹션 제목: “ObservableSignal — Adapter 구현 내부 개발자 로깅 API”

ObservableSignal은 Adapter 구현 코드 내부에서 개발자가 직접 운영 목적의 로그를 출력하는 정적 API입니다. Observable 래퍼가 설정한 공통 컨텍스트(request.layer, request.category.name, request.handler.name, request.handler.method)를 자동으로 포함합니다.

✅ = 지원, X = 미지원

LevelLoggingTracing (Activity Event)Metrics
DebugX (고빈도 → 노이즈)X
Warning✅ (span 내 열화 원인 추적)X
Error✅ (span 내 실패 원인 추적)X
  • Metrics 제외: Observable 래퍼가 request/response/duration 메트릭을 이미 자동 생성. 커스텀 메트릭은 IMeterFactory 직접 사용.
  • Debug에서 Tracing 제외: 캐시 미스 등 고빈도 이벤트를 span에 넣으면 trace 노이즈.
EventIdNameLevel설명
2021adapter.signal.debugDebug정상 흐름 상세 (캐시 미스, 쿼리 상세)
2022adapter.signal.warningWarning자동 복구 열화 (재시도, 폴백, rate limit)
2023adapter.signal.errorError복구 불가 실패 (재시도 소진, 서킷 오픈)
{request.layer} {request.category.name} {request.handler.name}.{request.handler.method} — {adapter.log.message} {@adapter.log.context}
프리픽스용도예시
adapter.retry.*재시도 관련adapter.retry.attempt, adapter.retry.delay_ms
adapter.http.*HTTP 관련adapter.http.status_code, adapter.http.retry_after_seconds
adapter.message.*메시지 브로커 관련adapter.message.id, adapter.message.queue
adapter.db.*데이터베이스 관련adapter.db.elapsed_ms, adapter.db.operation
adapter.cache.*캐시 관련adapter.cache.key, adapter.cache.provider
// Polly 재시도 시 Warning
ObservableSignal.Warning("Retry attempt {Attempt}/{MaxRetry} after {Delay}s delay",
("adapter.retry.attempt", attempt),
("adapter.retry.delay_ms", delay.TotalMilliseconds));
// 캐시 미스 시 Debug (고빈도)
ObservableSignal.Debug("Cache miss", ("adapter.cache.key", cacheKey));
// 재시도 소진 시 Error
ObservableSignal.Error(ex, "Database operation failed after exhausting retries",
("adapter.db.retry.attempt", maxRetries));
  1. [GenerateObservablePort] Source Generator가 ExecuteWithSpan 내에서 ObservableSignalScope.Begin()을 호출
  2. ObservableSignalScopeAsyncLocal로 현재 컨텍스트(logger, layer, category, handler, method)를 설정
  3. Adapter 코드에서 ObservableSignal.Debug/Warning/Error 호출 시 ObservableSignalScope.Current에서 공통 필드 획득
  4. ObservableSignalFactory가 ILogger + Activity Event로 출력
테스트파일
ObservableSignal API + ScopeTests/Functorium.Tests.Unit/DomainsTests/Observabilities/ObservableSignalTests.cs