From f10f78999dc001d10145bcb8c2f9f3685ed7d541 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Thu, 5 Dec 2024 17:12:35 +0100 Subject: [PATCH] Add basic telemetry --- app/jobs/telemetry_sending_job.rb | 11 +++++ app/services/telemetry/gather.rb | 32 +++++++++++++ app/services/telemetry/send.rb | 44 ++++++++++++++++++ config/initializers/01_constants.rb | 2 + ...5_add_devise_trackable_columns_to_users.rb | 13 ++++++ db/schema.rb | 9 +++- spec/jobs/telemetry_sending_job_spec.rb | 5 +++ spec/services/telemetry/gather_spec.rb | 45 +++++++++++++++++++ 8 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 app/jobs/telemetry_sending_job.rb create mode 100644 app/services/telemetry/gather.rb create mode 100644 app/services/telemetry/send.rb create mode 100644 db/migrate/20241205160055_add_devise_trackable_columns_to_users.rb create mode 100644 spec/jobs/telemetry_sending_job_spec.rb create mode 100644 spec/services/telemetry/gather_spec.rb diff --git a/app/jobs/telemetry_sending_job.rb b/app/jobs/telemetry_sending_job.rb new file mode 100644 index 00000000..fe9b74dd --- /dev/null +++ b/app/jobs/telemetry_sending_job.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class TelemetrySendingJob < ApplicationJob + queue_as :default + + def perform + data = Telemetry::Gather.new.call + + Telemetry::Send.new(data).call + end +end diff --git a/app/services/telemetry/gather.rb b/app/services/telemetry/gather.rb new file mode 100644 index 00000000..90b7ee01 --- /dev/null +++ b/app/services/telemetry/gather.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Telemetry::Gather + def initialize(measurement: 'dawarich_usage_metrics') + @measurement = measurement + end + + def call + { + measurement:, + timestamp: Time.current.to_i, + tags: { instance_id: }, + fields: { dau:, app_version: } + } + end + + private + + attr_reader :measurement + + def instance_id + @instance_id ||= Digest::SHA2.hexdigest(User.first.api_key) + end + + def app_version + "\"#{APP_VERSION}\"" + end + + def dau + User.where(last_sign_in_at: Time.zone.today.beginning_of_day..Time.zone.today.end_of_day).count + end +end diff --git a/app/services/telemetry/send.rb b/app/services/telemetry/send.rb new file mode 100644 index 00000000..c3cce833 --- /dev/null +++ b/app/services/telemetry/send.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class Telemetry::Send + BUCKET = 'dawarich_metrics' + ORG = 'monitoring' + + def initialize(payload) + @payload = payload + end + + def call + line_protocol = build_line_protocol + response = send_request(line_protocol) + handle_response(response) + end + + private + + attr_reader :payload + + def build_line_protocol + tag_string = payload[:tags].map { |k, v| "#{k}=#{v}" }.join(',') + field_string = payload[:fields].map { |k, v| "#{k}=#{v}" }.join(',') + + "#{payload[:measurement]},#{tag_string} #{field_string} #{payload[:timestamp].to_i}" + end + + def send_request(line_protocol) + HTTParty.post( + "#{TELEMETRY_URL}?org=#{ORG}&bucket=#{BUCKET}&precision=s", + body: line_protocol, + headers: { + 'Authorization' => "Token #{Base64.decode64(TELEMETRY_STRING)}", + 'Content-Type' => 'text/plain' + } + ) + end + + def handle_response(response) + Rails.logger.error("InfluxDB write failed: #{response.body}") unless response.success? + + response + end +end diff --git a/config/initializers/01_constants.rb b/config/initializers/01_constants.rb index 5065345f..d8cc2d81 100644 --- a/config/initializers/01_constants.rb +++ b/config/initializers/01_constants.rb @@ -6,3 +6,5 @@ PHOTON_API_HOST = ENV.fetch('PHOTON_API_HOST', nil) PHOTON_API_USE_HTTPS = ENV.fetch('PHOTON_API_USE_HTTPS', 'true') == 'true' DISTANCE_UNIT = ENV.fetch('DISTANCE_UNIT', 'km').to_sym APP_VERSION = File.read('.app_version').strip +TELEMETRY_STRING = Base64.encode64('IjVFvb8j3P9-ArqhSGav9j8YcJaQiuNIzkfOPKQDk2lvKXqb8t1NSRv50oBkaKtlrB_ZRzO9NdurpMtncV_HYQ==') +TELEMETRY_URL = 'https://influxdb2.frey.today/api/v2/write' diff --git a/db/migrate/20241205160055_add_devise_trackable_columns_to_users.rb b/db/migrate/20241205160055_add_devise_trackable_columns_to_users.rb new file mode 100644 index 00000000..80cccf4a --- /dev/null +++ b/db/migrate/20241205160055_add_devise_trackable_columns_to_users.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddDeviseTrackableColumnsToUsers < ActiveRecord::Migration[7.2] + def change + change_table :users, bulk: true do |t| + t.integer :sign_in_count, default: 0, null: false + t.datetime :current_sign_in_at + t.datetime :last_sign_in_at + t.string :current_sign_in_ip + t.string :last_sign_in_ip + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 3e1a538f..2927e2d5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_11_28_095325) do +ActiveRecord::Schema[7.2].define(version: 2024_12_05_160055) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -155,6 +155,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_28_095325) do t.bigint "user_id" t.jsonb "geodata", default: {}, null: false t.bigint "visit_id" + t.datetime "reverse_geocoded_at" t.index ["altitude"], name: "index_points_on_altitude" t.index ["battery"], name: "index_points_on_battery" t.index ["battery_status"], name: "index_points_on_battery_status" @@ -164,6 +165,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_28_095325) do t.index ["geodata"], name: "index_points_on_geodata", using: :gin t.index ["import_id"], name: "index_points_on_import_id" t.index ["latitude", "longitude"], name: "index_points_on_latitude_and_longitude" + t.index ["reverse_geocoded_at"], name: "index_points_on_reverse_geocoded_at" t.index ["timestamp"], name: "index_points_on_timestamp" t.index ["trigger"], name: "index_points_on_trigger" t.index ["user_id"], name: "index_points_on_user_id" @@ -208,6 +210,11 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_28_095325) do t.string "theme", default: "dark", null: false t.jsonb "settings", default: {"fog_of_war_meters"=>"100", "meters_between_routes"=>"1000", "minutes_between_routes"=>"60"} t.boolean "admin", default: false + t.integer "sign_in_count", default: 0, null: false + t.datetime "current_sign_in_at" + t.datetime "last_sign_in_at" + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end diff --git a/spec/jobs/telemetry_sending_job_spec.rb b/spec/jobs/telemetry_sending_job_spec.rb new file mode 100644 index 00000000..2e227710 --- /dev/null +++ b/spec/jobs/telemetry_sending_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe TelemetrySendingJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/services/telemetry/gather_spec.rb b/spec/services/telemetry/gather_spec.rb new file mode 100644 index 00000000..9b962113 --- /dev/null +++ b/spec/services/telemetry/gather_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Telemetry::Gather do + let!(:user) { create(:user, last_sign_in_at: Time.zone.today) } + + describe '#call' do + subject(:gather) { described_class.new.call } + + it 'returns a hash with measurement, timestamp, tags, and fields' do + expect(gather).to include(:measurement, :timestamp, :tags, :fields) + end + + it 'includes the correct measurement' do + expect(gather[:measurement]).to eq('dawarich_usage_metrics') + end + + it 'includes the current timestamp' do + expect(gather[:timestamp]).to be_within(1).of(Time.current.to_i) + end + + it 'includes the correct instance_id in tags' do + expect(gather[:tags][:instance_id]).to eq(Digest::SHA2.hexdigest(user.api_key)) + end + + it 'includes the correct app_version in fields' do + expect(gather[:fields][:app_version]).to eq("\"#{APP_VERSION}\"") + end + + it 'includes the correct dau in fields' do + expect(gather[:fields][:dau]).to eq(1) + end + + context 'with a custom measurement' do + let(:measurement) { 'custom_measurement' } + + subject(:gather) { described_class.new(measurement:).call } + + it 'includes the correct measurement' do + expect(gather[:measurement]).to eq('custom_measurement') + end + end + end +end