This commit is contained in:
Seth Call 2014-12-30 17:10:16 -06:00
parent 7ca3acb676
commit 786486026b
34 changed files with 399 additions and 17 deletions

View File

@ -72,6 +72,8 @@ gem 'iso-639'
gem 'rubyzip'
gem 'sanitize'
gem 'slim'
gem 'influxdb', '0.1.8'
gem 'influxdb-rails', '0.1.10'
group :libv8 do
gem 'libv8', "~> 3.11.8"

View File

@ -139,5 +139,13 @@ module JamAdmin
config.recurly_private_api_key = '7d623daabfc2434fa2a893bb008eb3e6'
# Use Public Keys to identify your site when using Recurly.js. See https://docs.recurly.com/js/#include to learn more.
config.recurly_public_api_key = 'sc-SZlO11shkeA1WMGuISLGg5'
# these values work out of the box with default settings of an influx install (you do have to add a development database by hand though)
config.influxdb_database = "development"
config.influxdb_username = "root"
config.influxdb_password = "root"
config.influxdb_hosts = ["localhost"]
config.influxdb_port = 8086
config.influxdb_ignored_environments = ENV["INFLUXDB_ENABLED"] == '1' ? ['test', 'cucumber'] : ['test', 'cucumber', 'development']
end
end

View File

@ -2,6 +2,7 @@
require File.expand_path('../application', __FILE__)
APP_CONFIG = Rails.application.config
Stats.client = InfluxDB::Rails.client
# Initialize the rails application
JamAdmin::Application.initialize!

View File

@ -0,0 +1,15 @@
InfluxDB::Rails.configure do |config|
config.influxdb_database = Rails.application.config.influxdb_database
config.influxdb_username = Rails.application.config.influxdb_username
config.influxdb_password = Rails.application.config.influxdb_password
config.influxdb_hosts = Rails.application.config.influxdb_hosts
config.influxdb_port = Rails.application.config.influxdb_port
config.ignored_environments = Rails.application.config.influxdb_ignored_environments
config.async = true
config.debug = false
config.logger = Logging.logger['InfluxDB']
config.series_name_for_controller_runtimes = "admin.rails.controller"
config.series_name_for_view_runtimes = "admin.rails.view"
config.series_name_for_db_runtimes = "admin.rails.db"
end

View File

@ -91,6 +91,7 @@ Logging::Rails.configure do |config|
#
Logging.logger.root.level = config.log_level
Logging.logger.root.appenders = config.log_to unless config.log_to.empty?
Logging.logger['InfluxDB'].level = :warn
# Under Phusion Passenger smart spawning, we need to reopen all IO streams
# after workers have forked.

View File

@ -48,6 +48,7 @@ gem 'rest-client'
gem 'iso-639'
gem 'rubyzip'
gem 'sanitize'
gem 'influxdb', '0.1.8'
group :test do
gem 'simplecov', '~> 0.7.1'

View File

@ -38,10 +38,10 @@ require "jam_ruby/lib/json_validator"
require "jam_ruby/lib/em_helper"
require "jam_ruby/lib/nav"
require "jam_ruby/lib/html_sanitize"
require "jam_ruby/resque/resque_hooks"
require "jam_ruby/resque/audiomixer"
require "jam_ruby/resque/quick_mixer"
require "jam_ruby/resque/icecast_config_writer"
require "jam_ruby/resque/resque_hooks"
require "jam_ruby/resque/scheduled/audiomixer_retry"
require "jam_ruby/resque/scheduled/icecast_config_retry"
require "jam_ruby/resque/scheduled/icecast_source_check"
@ -55,6 +55,7 @@ require "jam_ruby/resque/scheduled/active_music_session_cleaner"
require "jam_ruby/resque/scheduled/score_history_sweeper"
require "jam_ruby/resque/scheduled/scheduled_music_session_cleaner"
require "jam_ruby/resque/scheduled/recordings_cleaner"
require "jam_ruby/resque/scheduled/stats_maker"
require "jam_ruby/resque/google_analytics_event"
require "jam_ruby/resque/batch_email_job"
require "jam_ruby/mq_router"
@ -78,6 +79,7 @@ require "jam_ruby/app/uploaders/max_mind_release_uploader"
require "jam_ruby/lib/desk_multipass"
require "jam_ruby/lib/ip"
require "jam_ruby/lib/subscription_message"
require "jam_ruby/lib/stats.rb"
require "jam_ruby/amqp/amqp_connection_manager"
require "jam_ruby/database"
require "jam_ruby/message_factory"

View File

@ -0,0 +1,61 @@
require 'influxdb'
module JamRuby
class Stats
class << self
attr_accessor :client, :host
@@log = Logging.logger[JamRuby::Stats]
end
def self.destroy!
if @client
@client.stop!
end
end
def self.init(options)
influxdb_database = options[:influxdb_database]
influxdb_username = options[:influxdb_username]
influxdb_password = options[:influxdb_password]
influxdb_hosts = options[:influxdb_hosts]
influxdb_port = options[:influxdb_port]
influxdb_async = options[:influxdb_async].nil? ? true : options[:influxdb_async]
if influxdb_database && influxdb_database.length > 0
@client = InfluxDB::Client.new influxdb_database,
username: influxdb_username,
password: influxdb_password,
time_precision: 's',
hosts: influxdb_hosts,
port: influxdb_port,
async:influxdb_async,
retry: -1
@host = `hostname`.strip
else
@@log.debug("stats client not initiated")
end
end
def self.write(name, data)
if @client && data && data.length > 0
data['host'] = @host
data['time'] = Time.now.to_i
@client.write_point(name, data)
end
end
def self.timer(name)
start = Time.now
begin
yield
Stats.write(name, result: 'success', duration: Time.now - start)
rescue Exception => e
Stats.write(name, result: 'failure', duration: Time.now - start, error: e.to_s)
raise e
end
end
end
end

View File

@ -8,6 +8,7 @@ module JamRuby
TYPE_CLIENT = 'client'
TYPE_BROWSER = 'browser'
TYPE_LATENCY_TESTER = 'latency_tester'
CLIENT_TYPES = [TYPE_CLIENT, TYPE_BROWSER, TYPE_LATENCY_TESTER]
attr_accessor :joining_session
@ -20,7 +21,7 @@ module JamRuby
has_many :video_sources, :class_name => "JamRuby::VideoSource", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all
validates :as_musician, :inclusion => {:in => [true, false, nil]}
validates :client_type, :inclusion => {:in => [TYPE_CLIENT, TYPE_BROWSER, TYPE_LATENCY_TESTER]}
validates :client_type, :inclusion => {:in => CLIENT_TYPES}
validates_numericality_of :last_jam_audio_latency, greater_than:0, :allow_nil => true
validate :can_join_music_session, :if => :joining_session?
validate :user_or_latency_tester_present
@ -222,6 +223,28 @@ module JamRuby
update_locidispids
end
def self.stats
stats = {}
CLIENT_TYPES.each do |type|
stats[type] = 0
end
Connection.select('count(client_type) AS client_type_count, client_type') do |result|
stats[result['client_type']] = result['client_type_count']
end
result = Connection.select('count(id) AS total, count(scoring_timeout) AS scoring_timeout_count, count(music_session_id) AS in_session, count(as_musician) AS musicians, count(udp_reachable) AS udp_reachable_count, count(is_network_testing) AS is_network_testing_count').first
stats['count'] = result['total'].to_i
stats['scoring_timeout'] = result['scoring_timeout_count'].to_i
stats['in_session'] = result['in_session'].to_i
stats['musicians'] = result['musicians'].to_i
stats['udp_reachable'] = result['udp_reachable_count'].to_i
stats['networking_testing'] = result['is_network_testing_count'].to_i
stats
end
private
def require_at_least_one_track_when_in_session

View File

@ -1465,8 +1465,24 @@ module JamRuby
!approved_slots.blank?
end
# end devise compatibility
def self.stats
stats = {}
result = User.select('count(CASE WHEN musician THEN 1 ELSE null END) as musician_count, count(CASE WHEN musician = FALSE THEN 1 ELSE null END) as fan_count, count(first_downloaded_client_at) first_downloaded_client_at_count, count(first_ran_client_at) first_ran_client_at_count, count(first_certified_gear_at) first_certified_gear_at_count, count(first_music_session_at) as first_music_session_at_count, count(first_invited_at) first_invited_at_count, count(first_friended_at) as first_friended_at_count, count(first_social_promoted_at) first_social_promoted_at_count, avg(last_jam_audio_latency) last_jam_audio_latency_avg').first
puts "result #{result['musician_count']}"
stats['musicians'] = result['musician_count'].to_i
stats['fans'] = result['fan_count'].to_i
stats['downloaded_client'] = result['first_downloaded_client_at_count'].to_i
stats['ran_client'] = result['first_ran_client_at_count'].to_i
stats['certified_gear'] = result['first_certified_gear_at_count'].to_i
stats['jammed'] = result['first_music_session_at_count'].to_i
stats['invited'] = result['first_invited_at_count'].to_i
stats['friended'] = result['first_friended_at_count'].to_i
stats['social_promoted'] = result['first_social_promoted_at_count'].to_i
stats['audio_latency_avg'] = result['last_jam_audio_latency_avg'].to_f
stats
end
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64

View File

@ -8,6 +8,7 @@ module JamRuby
# executes a mix of tracks, creating a final output mix
class AudioMixer
extend JamRuby::ResqueStats
@queue = :audiomixer

View File

@ -3,6 +3,7 @@ require 'resque'
# more info on Measurement Protocol https://developers.google.com/analytics/devguides/collection/protocol/v1/
module JamRuby
class GoogleAnalyticsEvent
extend ResqueStats
@queue = :google_analytics_event

View File

@ -8,6 +8,7 @@ module JamRuby
# executes a mix of tracks, creating a final output mix
class QuickMixer
extend JamRuby::ResqueStats
@queue = :quick_mixer

View File

@ -1,10 +1,54 @@
require 'resque'
# https://devcenter.heroku.com/articles/forked-pg-connections
Resque.before_fork do
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
JamRuby::Stats.destroy!
end
Resque.after_fork do
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
config = {
influxdb_database: APP_CONFIG.influxdb_database,
influxdb_username: APP_CONFIG.influxdb_username,
influxdb_password: APP_CONFIG.influxdb_password,
influxdb_hosts: APP_CONFIG.influxdb_hosts,
influxdb_port: APP_CONFIG.influxdb_port,
influxdb_async: false # if we use async=true, the forked job will die before the stat is sent
}
JamRuby::Stats.init(config)
end
# for jobs that do not extend lonely job, just extend this module and get stats
module JamRuby
module ResqueStats
def around_perform(*args)
Stats.timer('job.stats') do
begin
yield
end
end
end
end
end
# for jobs that extend lonely job, we override around_perform already implemented in LonelyJob, and call into it
module Resque
module Plugins
module LonelyJob
def around_perform(*args)
Stats.timer('job.stats') do
begin
yield
ensure
unlock_queue(*args)
end
end
end
end
end
end

View File

@ -2,6 +2,9 @@
module JamRuby
class CleanupFacebookSignup
@queue = :scheduled_cleanup_facebook_signup
@@log = Logging.logger[CleanupFacebookSignup]

View File

@ -0,0 +1,23 @@
require 'json'
require 'resque'
require 'resque-retry'
require 'net/http'
require 'digest/md5'
module JamRuby
# creates stats to send to influx periodically
class StatsMaker
extend Resque::Plugins::LonelyJob
@queue = :stats_maker
@@log = Logging.logger['StatsMaker']
def self.perform
Stats.write('connection', Connection.stats)
Stats.write('users', User.stats)
end
end
end

View File

@ -0,0 +1,31 @@
require 'spec_helper'
describe Stats do
before(:each) do
Stats.client = nil
end
after(:all) do
Stats.client = nil
end
describe "not-inited" do
it "write" do
Stats.write('bleh', time: Time.now.to_i)
end
end
describe "inited" do
before(:each) do
Stats.init(influxdb_database: 'test')
end
it "write" do
# this can't pass unless there is an actual db listening
Stats.write('bleh', time: Time.now.to_i)
end
end
end

View File

@ -178,4 +178,20 @@ describe JamRuby::Connection do
end
end
describe "stats" do
it "no connections" do
stats = Connection.stats
stats[Connection::TYPE_CLIENT].should eq(0)
stats[Connection::TYPE_BROWSER].should eq(0)
stats[Connection::TYPE_LATENCY_TESTER].should eq(0)
stats['count'].should eq(0)
stats['scoring_timeout'].should eq(0)
stats['in_session'].should eq(0)
stats['musicians'].should eq(0)
stats['udp_reachable'].should eq(0)
stats['networking_testing'].should eq(0)
end
end
end

View File

@ -37,6 +37,39 @@ describe User do
end
end
describe "stats" do
it "no user" do
stats = User.stats
stats['musicians'].should == 0
stats['fans'].should == 0
stats['downloaded_client'].should == 0
stats['ran_client'].should == 0
stats['certified_gear'].should == 0
stats['invited'].should == 0
stats['friended'].should == 0
stats['social_promoted'].should == 0
stats['audio_latency_avg'].should == 0
end
it "single user" do
@user.musician = true
@user.last_jam_audio_latency = 5
@user.save!
stats = User.stats
@user.musician.should be_true
stats['musicians'].should == 1
stats['fans'].should == 0
stats['downloaded_client'].should == 0
stats['ran_client'].should == 0
stats['certified_gear'].should == 0
stats['invited'].should == 0
stats['friended'].should == 0
stats['social_promoted'].should == 0
stats['audio_latency_avg'].should == 5
end
end
describe "with admin attribute set to 'true'" do
before do
@user.save!

View File

@ -85,7 +85,8 @@ gem 'htmlentities'
gem 'sanitize'
gem 'recurly'
gem 'guard', '2.7.3'
gem 'influxdb-rails'
gem 'influxdb', '0.1.8'
gem 'influxdb-rails', '0.1.10'
group :development, :test do
gem 'rspec-rails', '2.14.2'

View File

@ -6,6 +6,7 @@
//= require fakeJamClientRecordings
//= require backend_alerts
//= require stun
//= requre influxdb-latest
(function (context, $) {
@ -26,6 +27,8 @@
context.JK.initJamClient(app);
updateScoringIntervals();
initializeInfluxDB();
})
$(document).on('JAMKAZAM_READY', function() {
@ -156,6 +159,17 @@
}
}
function initializeInfluxDB() {
context.stats = new InfluxDB({
"host" : gon.global.influxdb_host,
"port" : gon.global.influxdb_port,
"username" : gon.global.influxdb_username,
"password" : gon.global.influxdb_password,
"database" : gon.global.influxdb_database
});
context.stats.write = context.stats.write_point;
}
function initializeStun(app) {
stun = new context.JK.Stun(app);
context.JK.StunInstance = stun;

View File

@ -289,11 +289,14 @@ if defined?(Bundler)
config.jam_tracks_available = false
# these values work out of the box with default settings of an influx install
config.influxdb_database = "development"
# these values work out of the box with default settings of an influx install (you do have to add a development database by hand though)
config.influxdb_database = 'development'
config.influxdb_username = "root"
config.influxdb_password = "root"
config.influxdb_unsafe_username = "root" # these are exposed to JavaScript
config.influxdb_unsafe_password = "root" # these are exposed to JavaScript
config.influxdb_hosts = ["localhost"]
config.influxdb_port = 8086
config.influxdb_ignored_environments = ENV["INFLUXDB_ENABLED"] == '1' ? ['test', 'cucumber'] : ['test', 'cucumber', 'development']
end
end

View File

@ -3,7 +3,10 @@ require File.expand_path('../application', __FILE__)
Mime::Type.register "audio/ogg", :audio_ogg
# assign globals
APP_CONFIG = Rails.application.config
Stats.client = InfluxDB::Rails.client
# Initialize the rails application
SampleApp::Application.initialize!

View File

@ -14,6 +14,11 @@ unless $rails_rake_task
:connect_time_stale_browser => APP_CONFIG.websocket_gateway_connect_time_stale_browser,
:connect_time_expire_browser => APP_CONFIG.websocket_gateway_connect_time_expire_browser,
:max_connections_per_user => APP_CONFIG.websocket_gateway_max_connections_per_user,
:influxdb_database => APP_CONFIG.influxdb_database,
:influxdb_username => APP_CONFIG.influxdb_username,
:influxdb_password => APP_CONFIG.influxdb_password,
:influxdb_hosts => APP_CONFIG.influxdb_hosts,
:influxdb_port => APP_CONFIG.influxdb_port,
:rabbitmq_host => APP_CONFIG.rabbitmq_host,
:rabbitmq_port => APP_CONFIG.rabbitmq_port,
:calling_thread => current,

View File

@ -5,4 +5,9 @@ Gon.global.twitter_public_account = Rails.application.config.twitter_public_acco
Gon.global.scoring_get_work_interval = Rails.application.config.scoring_get_work_interval
Gon.global.scoring_get_work_backoff_interval = Rails.application.config.scoring_get_work_backoff_interval
Gon.global.ftue_network_test_min_wait_since_last_score = Rails.application.config.ftue_network_test_min_wait_since_last_score
Gon.global.influxdb_host = Rails.application.config.influxdb_hosts[0]
Gon.global.influxdb_port = Rails.application.config.influxdb_port
Gon.global.influxdb_database = Rails.application.config.influxdb_database
Gon.global.influxdb_username = Rails.application.config.influxdb_unsafe_username
Gon.global.influxdb_password = Rails.application.config.influxdb_unsafe_password
Gon.global.env = Rails.env

View File

@ -1,9 +1,13 @@
InfluxDB::Rails.configure do |config|
config.influxdb_database = Rails.application.config.influxdb_database
config.influxdb_username = Rails.application.config.influxdb_username
config.influxdb_password = Rails.application.config.influxdb_password
config.influxdb_hosts = Rails.application.config.influxdb_hosts
config.influxdb_port = Rails.application.config.influxdb_port
config.influxdb_database = Rails.application.config.influxdb_database
config.influxdb_username = Rails.application.config.influxdb_username
config.influxdb_password = Rails.application.config.influxdb_password
config.influxdb_hosts = Rails.application.config.influxdb_hosts
config.influxdb_port = Rails.application.config.influxdb_port
config.ignored_environments = Rails.application.config.influxdb_ignored_environments
config.async = true
config.debug = false
config.logger = Logging.logger['InfluxDB']
config.series_name_for_controller_runtimes = "web.rails.controller"
config.series_name_for_view_runtimes = "web.rails.view"

View File

@ -91,6 +91,9 @@ Logging::Rails.configure do |config|
#
Logging.logger.root.level = config.log_level
Logging.logger.root.appenders = config.log_to unless config.log_to.empty?
Logging.logger['ActiveSupport::Cache::FileStore'].level = :info
Logging.logger['ActiveSupport::OrderedOptions'].level = :warn
Logging.logger['InfluxDB'].level = :warn
# Under Phusion Passenger smart spawning, we need to reopen all IO streams
# after workers have forked.
@ -101,10 +104,6 @@ Logging::Rails.configure do |config|
# the file descriptors after forking ensures that each worker has a unique
# file descriptor.
#
Logging.logger['ActiveSupport::Cache::FileStore'].level = :info
Logging.logger['ActiveSupport::OrderedOptions'].level = :warn
if defined?(PhusionPassenger)
PhusionPassenger.on_event(:starting_worker_process) do |forked|
Logging.reopen if forked

View File

@ -64,3 +64,8 @@ RecordingsCleaner:
cron: 0 * * * *
class: "JamRuby::RecordingsCleaner"
description: "Cleans up recordings that no one wants after 7 days"
StatsMaker:
cron: "* * * * *"
class: "JamRuby::StatsMaker"
description: "Generates interesting stats from the database"

File diff suppressed because one or more lines are too long

View File

@ -53,6 +53,7 @@ gem 'iso-639'
gem 'language_list'
gem 'rubyzip'
gem 'sanitize'
gem 'influxdb', '0.1.8'
group :development do
gem 'pry'

View File

@ -67,5 +67,10 @@ Server.new.run(:port => config["port"] + (jam_instance-1 ) * 2,
:max_connections_per_user => config["max_connections_per_user"],
:rabbitmq_host => config['rabbitmq_host'],
:rabbitmq_port => config['rabbitmq_port'],
:influxdb_database => config['influxdb_database'],
:influxdb_username => config['influxdb_username'],
:influxdb_password => config['influxdb_password'],
:influxdb_hosts => config['influxdb_hosts'],
:influxdb_port => config['influxdb_port'],
:cidr => config['cidr'],
:gateway_name => gateway_name)

View File

@ -5,6 +5,11 @@ Defaults: &defaults
connect_time_expire_browser: 60
cidr: [0.0.0.0/0]
max_connections_per_user: 20
influxdb_database: "development"
influxdb_username: "root"
influxdb_password: "root"
influxdb_hosts: ["localhost"]
influxdb_port: 8086
development:
port: 6767

View File

@ -54,6 +54,11 @@ module JamWebsockets
@gateway_name = nil
@ar_base_logger = ::Logging::Repository.instance[ActiveRecord::Base]
@message_stats = {}
@login_success_count = 0
@login_fail_count = 0
@connected_count = 0
@disconnected_count = 0
end
def start(connect_time_stale_client, connect_time_expire_client, connect_time_stale_browser, connect_time_expire_browser, options={:host => "localhost", :port => 5672, :max_connections_per_user => 10, :gateway => 'default'}, &block)
@ -368,6 +373,8 @@ module JamWebsockets
client.onopen { |handshake|
stats_connected
# a unique ID for this TCP connection, to aid in debugging
client.channel_id = handshake.query["channel_id"]
@ -584,6 +591,7 @@ module JamWebsockets
})
if latency_tester.errors.any?
@log.warn "unable to log in latency_tester with errors: #{latency_tester.errors.inspect}"
stats_logged_in_failed
raise SessionError, "invalid login: #{latency_tester.errors.inspect}"
end
@ -604,6 +612,7 @@ module JamWebsockets
false,
latency_tester.id,
connection_expire_time)
stats_logged_in
send_to_client(client, login_ack)
end
end
@ -640,6 +649,7 @@ module JamWebsockets
# protect against this user swamping the server
if user && Connection.where(user_id: user.id).count >= @max_connections_per_user
@log.warn "user #{user.id}/#{user.email} unable to connect due to max_connections_per_user #{@max_connections_per_user}"
stats_logged_in_failed
raise SessionError, 'max_user_connections', 'max_user_connections'
end
@ -760,9 +770,11 @@ module JamWebsockets
user.id,
connection_expire_time,
client_update)
stats_logged_in
send_to_client(client, login_ack)
end
else
stats_logged_in_failed
raise SessionError.new('invalid login', 'invalid_login')
end
end
@ -1089,7 +1101,23 @@ module JamWebsockets
stats.map { |i| i[1] = (i[1] / 60.0).round(2) }
@log.info("msg/s: " + stats.map { |i| i.join('=>') }.join(', '));
# stuff in extra stats into the @message_stats and send it all off
@message_stats['gateway_name'] = @gateway_name
@message_stats['login'] = @login_success_count
@message_stats['login_fail'] = @login_fail_count
@message_stats['connected'] = @connected_count
@message_stats['disconnected'] = @disconnected_count
Stats.write('gateway.stats', @message_stats)
# clear out stats
@message_stats.clear
@login_success_count = 0
@login_fail_count = 0
@connected_count = 0
@disconnected_count = 0
end
def cleanup_clients_with_ids(expired_connections)
@ -1161,6 +1189,7 @@ module JamWebsockets
if pending
@log.debug "cleaned up not-logged-in client #{client}"
stats_disconnected
else
@log.debug "cleanup up logged-in client #{client}"
@ -1169,6 +1198,7 @@ module JamWebsockets
if context
remove_client(client.client_id)
remove_user(context)
stats_disconnected
else
@log.warn "skipping duplicate cleanup attempt of logged-in client"
end
@ -1176,6 +1206,21 @@ module JamWebsockets
end
end
def stats_logged_in
@login_success_count = @login_success_count + 1
end
def stats_logged_in_failed
@login_fail_count = @login_fail_count + 1
end
def stats_connected
@connected_count = @connected_count + 1
end
def stats_disconnected
@disconnected_count = @disconnected_count + 1
end
private

View File

@ -24,6 +24,9 @@ module JamWebsockets
gateway_name = options[:gateway_name]
rabbitmq_host = options[:rabbitmq_host]
rabbitmq_port = options[:rabbitmq_port].to_i
Stats::init(options)
calling_thread = options[:calling_thread]
trust_check = TrustCheck.new(trust_port, options[:cidr])