Skip to content

Commit

Permalink
Make metrics tracking async via a job
Browse files Browse the repository at this point in the history
  • Loading branch information
thatbudakguy committed Jan 5, 2024
1 parent 99d39dd commit 3ab6d83
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 55 deletions.
35 changes: 0 additions & 35 deletions app/controllers/concerns/metrics_concern.rb

This file was deleted.

10 changes: 7 additions & 3 deletions app/controllers/file_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
##
# API for delivering files from stacks
class FileController < ApplicationController
include MetricsConcern

rescue_from ActionController::MissingFile do
render plain: 'File not found', status: :not_found
end
Expand All @@ -19,7 +17,13 @@ def show
response.headers['Content-Length'] = current_file.content_length
response.headers.delete('X-Frame-Options')

track_download current_file.id, file: current_file.file_name
TrackDownloadJob.perform_later(
druid: current_file.id,
file: current_file.file_name,
user_agent: request.user_agent,
ip: request.remote_ip
)

send_file current_file.path, disposition:
end
# rubocop:enable Metrics/AbcSize
Expand Down
9 changes: 6 additions & 3 deletions app/controllers/iiif_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
##
# API for delivering IIIF-compatible images and image tiles
class IiifController < ApplicationController
include MetricsConcern

skip_forgery_protection

before_action :add_iiif_profile_header
Expand All @@ -26,7 +24,12 @@ def show

set_image_response_headers

track_download(identifier_params[:id], file: download_filename)
TrackDownloadJob.perform_later(
druid: identifier_params[:id],
file: download_filename,
user_agent: request.user_agent,
ip: request.remote_ip
)

self.content_type = Mime::Type.lookup_by_extension(iiif_params[:format]).to_s
self.status = projection.response.status
Expand Down
8 changes: 6 additions & 2 deletions app/controllers/object_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# API for delivering whole objects from stacks
class ObjectController < ApplicationController
include Zipline
include MetricsConcern

# Return a zip of all the files if they have access to all the files.
# This will force a login if any of the files is not access=world
Expand All @@ -23,7 +22,12 @@ def show
]
end

track_download druid
TrackDownloadJob.perform_later(
druid:,
user_agent: request.user_agent,
ip: request.remote_ip
)

zipline(zip_contents, "#{druid}.zip")
end

Expand Down
9 changes: 9 additions & 0 deletions app/jobs/application_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked

# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
end
18 changes: 18 additions & 0 deletions app/jobs/track_download_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

# Track a single download event by sending a request to the metrics API
class TrackDownloadJob < ApplicationJob
queue_as :default

rescue_from StandardError do |exception|
Rails.logger.error("Error sending metrics: #{exception}")
end

def perform(druid:, user_agent:, ip:, file: nil)
return true unless Settings.features.metrics == true

properties = { druid:, file: }.compact

MetricsService.new.track_event('download', properties, user_agent:, ip:)
end
end
38 changes: 28 additions & 10 deletions app/services/metrics_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,41 @@ def initialize(base_url: Settings.metrics_api_url)
@base_url = base_url
end

def track_event(data, original_request)
post_json('/ahoy/events', data, original_request)
def track_event(name, properties, user_agent:, ip:)
headers = {
'User-Agent': user_agent,
'X-Forwarded-For': ip
}

post_json('/ahoy/events', event_data(name, properties), headers)
end

private

def post_json(url, data, original_request)
# Schema: https://github.com/ankane/ahoy#events-1
# NOTE: it's possible to batch events this way.
def event_data(name, properties)
{
events: [
{
id: SecureRandom.uuid,
time: Time.current,
name:,
properties:
}
]
}
end

def default_headers
{ 'Content-Type': 'application/json' }
end

def post_json(url, data, headers)
connection.post(url) do |req|
# Pass the original browser info and IP along to the metrics API
req.headers['User-Agent'] = original_request.user_agent
req.headers['X-Forwarded-For'] = original_request.remote_ip
req.headers['Content-Type'] = 'application/json'
req.headers = default_headers.merge(headers)
req.body = data.to_json
end
rescue Faraday::ConnectionFailed => e
Rails.logger.error("Error sending metrics: #{e}")
nil
end

def connection
Expand Down
6 changes: 5 additions & 1 deletion config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,13 @@ class Application < Rails::Application
"172.20.21.192/28" # foa_lb_mgmt_prod_nets
].map { |proxy| IPAddr.new(proxy) }


# IIIF Auth v2 makes a request in one window to login and then opens a iframe to get a token.
# In order for this second request to know who the user is, the session token must created with SameSite=None
config.action_dispatch.cookies_same_site_protection = :none

# Use ActiveJob async adapter for all environments – our only jobs are
# for tracking metrics and they execute very quickly, so there's no need
# for a dedicated redis instance or similar
config.active_job.queue_adapter = :async
end
end
10 changes: 9 additions & 1 deletion spec/requests/metrics_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

require 'rails_helper'

RSpec.describe 'Metrics tracking', type: :request do
RSpec.describe 'Metrics tracking' do
include ActiveJob::TestHelper

let(:xml) do
<<-XML
<publicObject id="druid:xf680rd3068" published="2019-12-19T17:58:11Z" publishVersion="dor-services/8.1.1">
Expand Down Expand Up @@ -83,6 +85,8 @@
headers: { 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko)' },
env: { 'REMOTE_ADDR' => '73.235.188.148' }

perform_enqueued_jobs

expect(a_request(:post, 'https://example.com/ahoy/events').with do |req|
expect(req.body).to include '"name":"download"'
expect(req.body).to include '"druid":"xf680rd3068"'
Expand All @@ -98,6 +102,8 @@
headers: { 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko)' },
env: { 'REMOTE_ADDR' => '73.235.188.148' }

perform_enqueued_jobs

expect(a_request(:post, 'https://example.com/ahoy/events').with do |req|
expect(req.body).to include '"name":"download"'
expect(req.body).to include '"druid":"xf680rd3068"'
Expand Down Expand Up @@ -151,6 +157,8 @@
), headers: { 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko)' },
env: { 'REMOTE_ADDR' => '73.235.188.148' }

perform_enqueued_jobs

expect(a_request(:post, 'https://example.com/ahoy/events').with do |req|
expect(req.body).to include '"name":"download"'
expect(req.body).to include '"druid":"nr349ct7889"'
Expand Down

0 comments on commit 3ab6d83

Please sign in to comment.