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:

Process monitor graph