Track memory utilization of processes and graph the data via Chartkick, Highcharts, and Rails
In this post I’ll share some code to track the memory utilization of a number of processes and graph the data via Chartkick, Highcharts, and Rails for the backend.
Initial project setup:
# create directory and RVM files:
mkdir process_memory
echo ruby-2.3.1 > process_memory/.ruby-version
echo process_memory > process_memory/.ruby-gemset
cd process_memory
# rails 5 install & setup database
gem install rails
rails new . -d postgresql
rake db:create && rake db:migrate
Add Chatkick gem, edit file: Gemfile, add:
gem 'chartkick'
Execute bundle install
to install the new gem.
Download Highcharts JS file to: vendor/assets/javascripts/highcharts.js
Include the new javascript libraries, edit file: app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
//= require turbolinks:
// add:
//= require highcharts
//= require chartkick
//= require_tree .
Create the migrations for two new tables: one for the process name, and another for the memory profile entries. I had plans to track more information in both tables, but decided to simplify for this post. For instance: a custom regex for each process, and being able to track each process’s child processes separately.
class CreateProcessProperties < ActiveRecord::Migration[5.0]
def change
create_table :process_properties do |t|
t.string :name, null: false
t.timestamps
end
end
end
class CreateProcessMemoryItems < ActiveRecord::Migration[5.0]
def change
create_table :process_memory_items do |t|
t.references :process_property, null: false, index: true, foreign_key: true
t.bigint :memory, null: false
t.timestamps
end
end
end
Execute rake db:migrate
to create the new tables.
Add model validations and association to ProcessProperty, edit file: app/models/process_property.rb
class ProcessProperty < ApplicationRecord
has_many :process_memory_items, dependent: :destroy
validates :name, presence: true
def grep_name
@grep_name ||= "[#{name[0]}]#{name[1..-1]}"
end
end
Add model validations and association to ProcessMemoryItem, edit file: app/models/process_memory_item.rb
class ProcessMemoryItem < ApplicationRecord
belongs_to :process_property
validates :memory, numericality: true
validates :memory, presence: true
end
I then seeded my database, either via rails console
or in db/seeds.rb
ProcessProperty.create!(name: 'nginx')
ProcessProperty.create!(name: 'puma')
Next I created a new service class (ProcessMemoryService) to execute a ps command, parse the output, and create model data for each process name. new file: app/models/process_memory_service.rb
class ProcessMemoryService
PSLine = Struct.new(:pid, :ppid, :rss, :command)
def initialize
@process_properties = ProcessProperty.all
raise 'No processes to monitor.' if @process_properties.blank?
end
def run
loop do
@process_properties.each do |process_property|
output = execute_grep(process_property)
data = extract_data_from_output(output)
process_property.process_memory_items.create! memory: combine_data(data)
end
# note: sleeping for a minute it not perfect because it doesn't account
# for the time it takes per code iteration, but good enough for this post
sleep 1.minute
end
end
private
def combine_data(data)
data.map(&:rss).sum
end
def execute_grep(process_property)
command = "ps ax -o pid,ppid,rss,command | egrep -i \"#{process_property.grep_name}\""
`#{command}`.strip.split("\n")
end
def extract_data_from_output(output)
return [] if output.blank?
output.map { |line| split_line(line) }
end
def split_line(line)
data = line.match /^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/
PSLine.new(data[1].to_i, data[2].to_i, data[3].to_i * 1024, data[4])
end
end
Create a new rake task to run the new service, new file: lib/tasks/monitor.rake
namespace :monitor do
desc 'Monitor process memory'
task process_memory: :environment do
ProcessMemoryService.new.run
end
end
Started the monitoring service via: rake monitor:process_memory
. I left this running to collect data for an extended period.
I then shifted gears to build the frontend. I added a class method to ProcessMemoryItem to join the tables together, group by process name, and then collect the data per process by created_at and memory usage. edit file: app/models/process_memory_item.rb
class ProcessMemoryItem < ApplicationRecord
belongs_to :process_property
# new class method:
def self.chart_data
select('process_properties.name', 'process_memory_items.memory', 'process_memory_items.created_at')
.joins(:process_property)
.order(created_at: :asc)
.group_by(&:name)
.each_with_object([]) do |(process_name, data), ary|
ary << {
name: process_name,
data: data.map {|pmi| [pmi.created_at, pmi.memory] }
}
end
end
end
I added a new controller to execute the above class method and pass the data to an ERB view, new file: app/controllers/process_memories_controller.rb
class ProcessMemoriesController < ApplicationController
def index
@process_memory_data = ProcessMemoryItem.chart_data
end
end
Contents of new ERB file, app/views/process_memories/index.html.erb
<%= line_chart @process_memory_data %>
And last added the controller route, edit file: config/routes.rb
Rails.application.routes.draw do
root 'process_memories#index'
end
Started rails via rails s
, browsed to http://localhost:3000 to see the memory utilization graphed: