액티브 서포트 계측

액티브 서포트는 Ruby 언어 확장, 유틸리티 및 기타 기능을 제공하는 Rails의 핵심 부분입니다. 포함된 기능 중 하나는 Ruby 코드 내부의 특정 작업을 측정하기 위한 계측 API입니다. 이는 Rails 애플리케이션 또는 프레임워크 자체 내부에서 사용할 수 있습니다. 그러나 Rails에 국한되지 않으며 다른 Ruby 스크립트에서도 독립적으로 사용할 수 있습니다.

이 가이드에서는 액티브 서포트의 계측 API를 사용하여 Rails 및 기타 Ruby 코드 내부의 이벤트를 측정하는 방법을 배울 것입니다.

이 가이드를 읽고 나면 다음을 알 수 있습니다:

  • 계측이 제공할 수 있는 것.
  • 훅에 구독자를 추가하는 방법.
  • Rails 프레임워크 내의 계측 훅.
  • 사용자 정의 계측 구현을 구축하는 방법.

계측 소개

액티브 서포트가 제공하는 계측 API를 통해 개발자는 다른 개발자가 훅에 연결할 수 있는 훅을 제공할 수 있습니다. Rails 프레임워크 내에는 여러 개의 이러한 훅이 있습니다. 이 API를 사용하면 개발자는 애플리케이션 또는 다른 Ruby 코드 내부에서 특정 이벤트가 발생할 때 알림을 받도록 선택할 수 있습니다.

예를 들어, Active Record 내에는 Active Record가 데이터베이스에서 SQL 쿼리를 사용할 때마다 호출되는 이 있습니다. 이 훅에 구독하여 특정 작업 중 쿼리 수를 추적할 수 있습니다. 또한 컨트롤러 작업 처리에 대한 훅이 있습니다. 이를 사용하여 특정 작업의 실행 시간을 추적할 수 있습니다.

또한 애플리케이션 내에서 사용자 정의 이벤트를 만들고 나중에 구독할 수 있습니다.

이벤트 구독하기

단일 인수 블록을 사용하여 ActiveSupport::Notifications.subscribe를 사용하면 모든 알림을 수신할 수 있습니다. 블록이 받는 인수 수에 따라 다른 데이터를 받게 됩니다.

이벤트를 구독하는 첫 번째 방법은 단일 인수 블록을 사용하는 것입니다. 인수는 ActiveSupport::Notifications::Event 인스턴스가 됩니다.

ActiveSupport::Notifications.subscribe "process_action.action_controller" do |event|
  event.name        # => "process_action.action_controller"
  event.duration    # => 10 (in milliseconds)
  event.allocations # => 1826
  event.payload     # => {:extra=>information}

  Rails.logger.info "#{event} Received!"
end

이벤트 객체의 모든 데이터가 필요하지 않은 경우 다음과 같이 5개의 인수를 받는 블록을 지정할 수 있습니다:

  • 이벤트 이름
  • 시작 시간
  • 종료 시간
  • 이벤트를 발생시킨 계측기의 고유 ID
  • 이벤트 페이로드
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, unique_id, payload|
  # 사용자 정의 코드
  Rails.logger.info "#{name} Received! (started: #{started}, finished: #{finished})" # process_action.action_controller Received! (started: 2019-05-05 13:43:57 -0800, finished: 2019-05-05 13:43:58 -0800)
end

정확한 경과 시간을 계산하려면 ActiveSupport::Notifications.monotonic_subscribe를 사용하세요. 제공된 블록은 위와 동일한 인수를 받지만 startedfinished에는 정확한 단조 시간 값이 포함됩니다.

ActiveSupport::Notifications.monotonic_subscribe "process_action.action_controller" do |name, started, finished, unique_id, payload|
  # 사용자 정의 코드
  duration = finished - started # 1560979.429234 - 1560978.425334
  Rails.logger.info "#{name} Received! (duration: #{duration})" # process_action.action_controller Received! (duration: 1.0039)
end

정규 표현식과 일치하는 이벤트에 구독할 수도 있습니다. 이를 통해 한 번에 여러 이벤트에 구독할 수 있습니다. 다음은 ActionController의 모든 이벤트에 구독하는 방법입니다:

ActiveSupport::Notifications.subscribe(/action_controller/) do |event|
  # ActionController 이벤트 모두 검사
end

Rails 프레임워크 훅

Ruby on Rails 프레임워크 내에는 일반적인 이벤트에 대한 여러 훅이 제공됩니다. 이러한 이벤트와 해당 페이로드는 아래에 자세히 설명되어 있습니다.

Action Controller

start_processing.action_controller

:controller 컨트롤러 이름
:action 작업
:request ActionDispatch::Request 객체
:params 필터링된 매개변수를 제외한 요청 매개변수 해시
:headers 요청 헤더
:format html/js/json/xml 등
:method HTTP 요청 동사
:path 요청 경로
{
  controller: "PostsController",
  action: "new",
  params: { "action" => "new", "controller" => "posts" },
  headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>,
  format: :html,
  method: "GET",
  path: "/posts/new"
}

process_action.action_controller

:controller 컨트롤러 이름
:action 작업
:params 필터링된 매개변수를 제외한 요청 매개변수 해시
:headers 요청 헤더
:format html/js/json/xml 등
:method HTTP 요청 동사
:path 요청 경로
:request ActionDispatch::Request 객체
:response ActionDispatch::Response 객체
:status HTTP 상태 코드
:view_runtime 뷰에서 소요된 시간(ms)
:db_runtime 데이터베이스 쿼리 실행에 소요된 시간(ms)
{
  controller: "PostsController",
  action: "index",
  params: {"action" => "index", "controller" => "posts"},
  headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>,
  format: :html,
  method: "GET",
  path: "/posts",
  request: #<ActionDispatch::Request:0x00007ff1cb9bd7b8>,
  response: #<ActionDispatch::Response:0x00007f8521841ec8>,
  status: 200,
  view_runtime: 46.848,
  db_runtime: 0.157
}

send_file.action_controller

:path 파일의 전체 경로

추가 키는 호출자에 의해 추가될 수 있습니다.

send_data.action_controller

ActionController는 페이로드에 특정 정보를 추가하지 않습니다. 모든 옵션이 페이로드로 전달됩니다.

redirect_to.action_controller

:status HTTP 응답 코드
:location 리디렉션할 URL
:request ActionDispatch::Request 객체
{
  status: 302,
  location: "http://localhost:3000/posts/new",
  request: <ActionDispatch::Request:0x00007ff1cb9bd7b8>
}

halted_callback.action_controller

:filter 작업을 중단시킨 필터
{
  filter: ":halting_filter"
}

unpermitted_parameters.action_controller

:keys 허용되지 않은 키
:context 다음 키를 포함하는 해시: :controller, :action, :params, :request

send_stream.action_controller

:filename 파일 이름
:type HTTP 콘텐츠 유형
:disposition HTTP 콘텐츠 디스포지션
{
  filename: "subscribers.csv",
  type: "text/csv",
  disposition: "attachment"
}

Action Controller: 캐싱

write_fragment.action_controller

:key 전체 키
{
  key: 'posts/1-dashboard-view'
}

read_fragment.action_controller

:key 전체 키
{
  key: 'posts/1-dashboard-view'
}

expire_fragment.action_controller

:key 전체 키
{
  key: 'posts/1-dashboard-view'
}

exist_fragment?.action_controller

:key 전체 키
{
  key: 'posts/1-dashboard-view'
}

Action Dispatch

process_middleware.action_dispatch

:middleware 미들웨어 이름

redirect.action_dispatch

:status HTTP 응답 코드
:location 리디렉션할 URL
:request ActionDispatch::Request 객체

request.action_dispatch

:request ActionDispatch::Request 객체

Action View

render_template.action_view

:identifier 템플릿의 전체 경로
:layout 적용되는 레이아웃
:locals 템플릿에 전달된 지역 변수
{
  identifier: "/Users/adam/projects/notifications/app/views/posts/index.html.erb",
  layout: "layouts/application",
  locals: { foo: "bar" }
}

render_partial.action_view

:identifier 템플릿의 전체 경로
:locals 템플릿에 전달된 지역 변수
{
  identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb",
  locals: { foo: "bar" }
}

render_collection.action_view

:identifier 템플릿의 네, 계속해서 번역하겠습니다.

render_collection.action_view

:identifier 템플릿의 전체 경로
:count 컬렉션의 크기
:cache_hits 캐시에서 가져온 부분 템플릿 수

:cache_hits 키는 컬렉션이 cached: true로 렌더링된 경우에만 포함됩니다.

{
  identifier: "/Users/adam/projects/notifications/app/views/posts/_post.html.erb",
  count: 3,
  cache_hits: 0
}

render_layout.action_view

:identifier 템플릿의 전체 경로
{
  identifier: "/Users/adam/projects/notifications/app/views/layouts/application.html.erb"
}

Active Record

sql.active_record

:sql SQL 문
:name 작업 이름
:connection 연결 객체
:binds 바인딩 매개변수
:type_casted_binds 유형 변환된 바인딩 매개변수
:statement_name SQL 문 이름
:async 쿼리가 비동기적으로 로드되면 true
:cached 캐시된 쿼리가 사용되면 true가 추가됨
:row_count 쿼리에 의해 반환된 행 수

어댑터는 자체 데이터를 추가할 수 있습니다.

{
  sql: "SELECT \"posts\".* FROM \"posts\" ",
  name: "Post Load",
  connection: <ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x00007f9f7a838850>,
  binds: [<ActiveModel::Attribute::WithCastValue:0x00007fe19d15dc00>],
  type_casted_binds: [11],
  statement_name: nil,
  row_count: 5
}

strict_loading_violation.active_record

이 이벤트는 config.active_record.action_on_strict_loading_violation:log로 설정된 경우에만 발생합니다.

:owner strict_loading이 활성화된 모델
:reflection 로드하려고 시도한 연관 관계의 리플렉션

instantiation.active_record

:record_count 인스턴스화된 레코드 수
:class_name 레코드의 클래스
{
  record_count: 1,
  class_name: "User"
}

Action Mailer

deliver.action_mailer

:mailer 메일러 클래스 이름
:message_id Mail 젬에 의해 생성된 메시지 ID
:subject 메일 제목
:to 메일 수신자 주소
:from 메일 발신자 주소
:bcc 메일 숨은 참조 주소
:cc 메일 참조 주소
:date 메일 날짜
:mail 메일의 인코딩된 형식
:perform_deliveries 이 메시지 전송 수행 여부
{
  mailer: "Notification",
  message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
  subject: "Rails Guides",
  to: ["users@rails.com", "dhh@rails.com"],
  from: ["me@rails.com"],
  date: Sat, 10 Mar 2012 14:18:09 +0100,
  mail: "...", # 간단히 생략
  perform_deliveries: true
}

process.action_mailer

:mailer 메일러 클래스 이름
:action 작업
:args 인수
{
  mailer: "Notification",
  action: "welcome_email",
  args: []
}

Active Support: 캐싱

cache_read.active_support

:key 저장소에서 사용된 키
:store 저장소 클래스 이름
:hit 이 읽기가 히트인지 여부
:super_operation fetch로 읽기가 수행된 경우 :fetch

cache_read_multi.active_support

:key 저장소에서 사용된 키
:store 저장소 클래스 이름
:hits 캐시 히트 키
:super_operation fetch_multi로 읽기가 수행된 경우 :fetch_multi

cache_generate.active_support

이 이벤트는 fetch가 블록과 함께 호출될 때만 발생합니다.

:key 저장소에서 사용된 키
:store 저장소 클래스 이름

fetch에 전달된 옵션은 저장소에 쓸 때 페이로드에 병합됩니다.

{
  key: "name-of-complicated-computation",
  store: "ActiveSupport::Cache::MemCacheStore"
}

cache_fetch_hit.active_support

이 이벤트는 fetch가 블록과 함께 호출될 때만 발생합니다.

:key 저장소에서 사용된 키
:store 저장소 클래스 이름

fetch에 전달된 옵션은 페이로드에 병합됩니다.

{
  key: "name-of-complicated-computation",
  store: "ActiveSupport::Cache::MemCacheStore"
}

cache_write.active_support

:key 저장소에서 사용된 키
:store 저장소 클래스 이름

캐시 저장소는 자체 데이터를 추가할 수 있습니다.

{
  key: "name-of-complicated-computation",
  store: "ActiveSupport::Cache::MemCacheStore"
}

cache_write_multi.active_support

:key 저장소에 쓰여진 키와 값
:store 저장소 클래스 이름

cache_increment.active_support

이 이벤트는 MemCacheStore 또는 RedisCacheStore를 사용할 때만 발생합니다.

:key 저장소에서 사용된 키
:store 저장소 클래스 이름
:amount 증가 금액
{
  key: "bottles-of-beer",
  store: "ActiveSupport::Cache::RedisCacheStore",
  amount: 99
}

cache_decrement.active_support

이 이벤트는 Memcached 또는 Redis 캐시 저장소를 사용할 때만 발생합니다.

:key 저장소에서 사용된 키
:store 저장소 클래스 이름
:amount 감소 금액
{
  key: "bottles-of-beer",
  store: "ActiveSupport::Cache::RedisCacheStore",
  amount: 1
}

cache_delete.active_support

:key 저장소에서 사용된 키
:store 저장소 클래스 이름
{
  key: "name-of-complicated-computation",
  store: "ActiveSupport::Cache::MemCacheStore"
}

cache_delete_multi.active_support

:key 저장소에서 사용된 키
:store 저장소 클래스 이름

cache_delete_matched.active_support

이 이벤트는 RedisCacheStore, FileStore 또는 MemoryStore를 사용할 때만 발생합니다.

:key 사용된 키 패턴
:store 저장소 클래스 이름
{
  key: "posts/*",
  store: "ActiveSupport::Cache::RedisCacheStore"
}

cache_cleanup.active_support

이 이벤트는 MemoryStore를 사용할 때만 발생합니다.

:store 저장소 클래스 이름
:size 정리 전 캐시의 항목 수
{
  store: "ActiveSupport::Cache::MemoryStore",
  size: 9001
}

cache_prune.active_support

이 이벤트는 MemoryStore를 사용할 때만 발생합니다.

:store 저장소 클래스 이름
:key 캐시의 대상 크기(바이트)
:from 정리 전 캐시의 크기(바이트)
{
  store: "ActiveSupport::Cache::MemoryStore",
  key: 5000,
  from: 9001
}

cache_exist?.active_support

:key 저장소에서 사용된 키
:store 저장소 클래스 이름
{
  key: "name-of-complicated-computation",
  store: "ActiveSupport::Cache::MemCacheStore"
}

Active Support: 메시지

message_serializer_fallback.active_support

:serializer 기본(의도된) 직렬화기
:fallback 대체(실제) 직렬화기
:serialized 직렬화된 문자열
:deserialized 역직렬화된 값
{
  serializer: :json_allow_marshal,
  fallback: :marshal, 계속해서 번역하겠습니다.

### Active Support: 메시지

#### `message_serializer_fallback.active_support`

|              |                          |
| --------------- | ----------------------------- |
| `:serializer`   | 기본(의도된) 직렬화기        |
| `:fallback`     | 대체(실제) 직렬화기         |
| `:serialized`   | 직렬화된 문자열             |
| `:deserialized` | 역직렬화된                |

```ruby
{
  serializer: :json_allow_marshal,
  fallback: :marshal,
  serialized: "\x04\b{\x06I\"\nHello\x06:\x06ETI\"\nWorld\x06;\x00T",
  deserialized: { "Hello" => "World" },
}

Active Job

enqueue_at.active_job

:adapter 작업을 처리하는 QueueAdapter 객체
:job 작업 객체

enqueue.active_job

:adapter 작업을 처리하는 QueueAdapter 객체
:job 작업 객체

enqueue_retry.active_job

:job 작업 객체
:adapter 작업을 처리하는 QueueAdapter 객체
:error 재시도를 유발한 오류
:wait 재시도 지연 시간

enqueue_all.active_job

:adapter 작업을 처리하는 QueueAdapter 객체
:jobs 작업 객체 배열

perform_start.active_job

:adapter 작업을 처리하는 QueueAdapter 객체
:job 작업 객체

perform.active_job

:adapter 작업을 처리하는 QueueAdapter 객체
:job 작업 객체
:db_runtime 데이터베이스 쿼리 실행에 소요된 시간(ms)

retry_stopped.active_job

:adapter 작업을 처리하는 QueueAdapter 객체
:job 작업 객체
:error 재시도를 중지시킨 오류

discard.active_job

:adapter 작업을 처리하는 QueueAdapter 객체
:job 작업 객체
:error 작업 폐기를 유발한 오류

Action Cable

perform_action.action_cable

:channel_class 채널 클래스 이름
:action 작업
:data 데이터 해시

transmit.action_cable

:channel_class 채널 클래스 이름
:data 데이터 해시
:via 경로

transmit_subscription_confirmation.action_cable

:channel_class 채널 클래스 이름

transmit_subscription_rejection.action_cable

:channel_class 채널 클래스 이름

broadcast.action_cable

:broadcasting 명명된 브로드캐스팅
:message 메시지 해시
:coder 코더

Active Storage

preview.active_storage

:key 보안 토큰

transform.active_storage

analyze.active_storage

:analyzer 분석기 이름(예: ffprobe)

Active Storage: 저장소 서비스

service_upload.active_storage

:key 보안 토큰
:service 서비스 이름
:checksum 무결성 확인을 위한 체크섬

service_streaming_download.active_storage

:key 보안 토큰
:service 서비스 이름

service_download_chunk.active_storage

:key 보안 토큰
:service 서비스 이름
:range 읽으려고 시도한 바이트 범위

service_download.active_storage

:key 보안 토큰
:service 서비스 이름

service_delete.active_storage

:key 보안 토큰
:service 서비스 이름

service_delete_prefixed.active_storage

:prefix 키 접두사
:service 서비스 이름

service_exist.active_storage

:key 보안 토큰
:service 서비스 이름
:exist 파일 또는 blob이 존재하는지

service_url.active_storage

:key 보안 토큰
:service 서비스 이름
:url 생성된 URL

service_update_metadata.active_storage

이 이벤트는 Google Cloud Storage 서비스를 사용할 때만 발생합니다.

:key 보안 토큰
:service 서비스 이름
:content_type HTTP Content-Type 필드
:disposition HTTP Content-Disposition 필드

Action Mailbox

process.action_mailbox

:mailbox ActionMailbox::Base를 상속받는 Mailbox 클래스 인스턴스
:inbound_email 처리 중인 수신 이메일에 대한 데이터 해시
{
  mailbox: #<RepliesMailbox:0x00007f9f7a8388>,
  inbound_email: {
    id: 1,
    message_id: "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com",
    status: "processing"
  }
}

Railties

load_config_initializer.railties

:initializer config/initializers에 로드된 초기화기 경로

Rails

deprecation.rails

:message 감사 경고 메시지
:callstack 감사가 발생한 위치
:gem_name 감사를 보고한 젬 이름
:deprecation_horizon 제거될 예정인 동작 버전

예외

계측 중 예외가 발생하면 페이로드에 예외 정보가 포함됩니다.

:exception 두 개의 요소로 이루어진 배열: 예외 클래스 이름과 메시지
:exception_object 예외 객체

사용자 정의 이벤트 만들기

자체 이벤트를 추가하는 것도 쉽습니다. Active Support가 모든 복잡한 작업을 처리해 줍니다. ActiveSupport::Notifications.instrumentname, payload, 그리고 블록과 함께 호출하면 됩니다. 블록이 반환된 후 알림이 전송됩니다. Active Support는 시작 및 종료 시간을 생성하고 계측기의 고유 ID를 추가합니다. instrument 호출에 전달된 모든 데이터는 페이로드에 포함됩니다.

예:

ActiveSupport::Notifications.instrument "my.custom.event", this: :data do
  # 사용자 정의 코드 작성
end

이제 다음과 같이 이 이벤트를 수신할 수 있습니다:

ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, finished, unique_id, data|
  puts data.inspect # {:this=>:data}
end

블록 없이 instrument를 호출할 수도 있습니다. 이렇게 하면 계측 인프라를 다른 메시징 용도로 활용할 수 있습니다.

ActiveSupport::Notifications.instrument "my.custom.event", this: :data

ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, finished, unique_id, data|
  puts data.inspect # {:this=>:data}
end

사용자 정의 이벤트를 정의할 때는 Rails 규칙을 따르는 것이 좋습니다. 형식은 event.library입니다. 애플리케이션이 트윗을 보내는 경우 tweet.twitter와 같은 이벤트를 만들어야 합니다.