(MAINT) Add optional API Request Logging

This was partially an exercise to use middleware, but also to enable
basic logging for the API by logging all the API calls to the logger.
This commit is contained in:
John O'Connor 2020-06-26 20:49:17 +01:00
parent a21d8c5642
commit 85ff3f7022
17 changed files with 85 additions and 24 deletions

View file

@ -27,13 +27,13 @@ end
if torun.include? :api if torun.include? :api
api = Thread.new do api = Thread.new do
redis = Vmpooler.new_redis(redis_host, redis_port, redis_password) redis = Vmpooler.new_redis(redis_host, redis_port, redis_password)
Vmpooler::API.execute(torun, config, redis, metrics) Vmpooler::API.execute(torun, config, redis, metrics, logger)
end end
torun_threads << api torun_threads << api
elsif metrics.respond_to?(:setup_prometheus_metrics) elsif metrics.respond_to?(:setup_prometheus_metrics)
# Run the cut down API - Prometheus Metrics only. # Run the cut down API - Prometheus Metrics only.
prometheus_only_api = Thread.new do prometheus_only_api = Thread.new do
Vmpooler::API.execute(torun, config, nil, metrics) Vmpooler::API.execute(torun, config, nil, metrics, logger)
end end
torun_threads << prometheus_only_api torun_threads << prometheus_only_api
end end

View file

@ -191,6 +191,13 @@ https://jenkins.example.com/job/platform\_puppet-agent-extra\_puppet-agent-integ
Expects a boolean value Expects a boolean value
(optional; default: false) (optional; default: false)
### REQUEST\_LOGGER
Enable API request logging to the logger
When enabled all requests to the API are written to the standard logger.
Expects a boolean value
(optional; default: false)
### EXTRA\_CONFIG ### EXTRA\_CONFIG
Specify additional application configuration files Specify additional application configuration files

View file

@ -84,6 +84,7 @@ module Vmpooler
parsed_config[:config]['experimental_features'] = ENV['EXPERIMENTAL_FEATURES'] if ENV['EXPERIMENTAL_FEATURES'] parsed_config[:config]['experimental_features'] = ENV['EXPERIMENTAL_FEATURES'] if ENV['EXPERIMENTAL_FEATURES']
parsed_config[:config]['purge_unconfigured_folders'] = ENV['PURGE_UNCONFIGURED_FOLDERS'] if ENV['PURGE_UNCONFIGURED_FOLDERS'] parsed_config[:config]['purge_unconfigured_folders'] = ENV['PURGE_UNCONFIGURED_FOLDERS'] if ENV['PURGE_UNCONFIGURED_FOLDERS']
parsed_config[:config]['usage_stats'] = ENV['USAGE_STATS'] if ENV['USAGE_STATS'] parsed_config[:config]['usage_stats'] = ENV['USAGE_STATS'] if ENV['USAGE_STATS']
parsed_config[:config]['request_logger'] = ENV['REQUEST_LOGGER'] if ENV['REQUEST_LOGGER']
parsed_config[:redis] = parsed_config[:redis] || {} parsed_config[:redis] = parsed_config[:redis] || {}
parsed_config[:redis]['server'] = ENV['REDIS_SERVER'] || parsed_config[:redis]['server'] || 'localhost' parsed_config[:redis]['server'] = ENV['REDIS_SERVER'] || parsed_config[:redis]['server'] || 'localhost'

View file

@ -9,7 +9,7 @@ module Vmpooler
# Load dashboard components # Load dashboard components
require 'vmpooler/dashboard' require 'vmpooler/dashboard'
def self.execute(torun, config, redis, metrics) def self.execute(torun, config, redis, metrics, logger)
self.settings.set :config, config self.settings.set :config, config
self.settings.set :redis, redis unless redis.nil? self.settings.set :redis, redis unless redis.nil?
self.settings.set :metrics, metrics self.settings.set :metrics, metrics
@ -43,6 +43,9 @@ module Vmpooler
end end
if torun.include? :api if torun.include? :api
# Enable API request logging only if required
use Vmpooler::API::RequestLogger, logger: logger if config[:config]['request_logger']
use Vmpooler::Dashboard use Vmpooler::Dashboard
use Vmpooler::API::Dashboard use Vmpooler::API::Dashboard
use Vmpooler::API::Reroute use Vmpooler::API::Reroute

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Vmpooler
class API
class RequestLogger
attr_reader :app
def initialize(app, options = {})
@app = app
@logger = options[:logger]
end
def call(env)
status, headers, body = @app.call(env)
@logger.log('s', "[ ] API: Method: #{env['REQUEST_METHOD']}, Status: #{status}, Path: #{env['PATH_INFO']}, Body: #{body}")
[status, headers, body]
end
end
end
end

View file

@ -6,9 +6,10 @@
# #
# The code was also failing Rubocop on PR check, so have addressed all the offenses. # The code was also failing Rubocop on PR check, so have addressed all the offenses.
# #
# The method strip_ids_from_path has been adapted to add a match for hostnames in paths # The method strip_hostnames_from_path (originally strip_ids_from_path) has been adapted
# to replace with a single ":hostname" string to avoid proliferation of stat lines for # to add a match for hostnames in paths # to replace with a single ":hostname" string to
# each new vm hostname deleted, modified or otherwise queried. # avoid # proliferation of stat lines for # each new vm hostname deleted, modified or
# otherwise queried.
require 'benchmark' require 'benchmark'
require 'prometheus/client' require 'prometheus/client'
@ -91,12 +92,12 @@ module Vmpooler
counter_labels = { counter_labels = {
code: code, code: code,
method: env['REQUEST_METHOD'].downcase, method: env['REQUEST_METHOD'].downcase,
path: strip_ids_from_path(env['PATH_INFO']) path: strip_hostnames_from_path(env['PATH_INFO'])
} }
duration_labels = { duration_labels = {
method: env['REQUEST_METHOD'].downcase, method: env['REQUEST_METHOD'].downcase,
path: strip_ids_from_path(env['PATH_INFO']) path: strip_hostnames_from_path(env['PATH_INFO'])
} }
@requests.increment(labels: counter_labels) @requests.increment(labels: counter_labels)
@ -105,10 +106,11 @@ module Vmpooler
nil nil
end end
def strip_ids_from_path(path) def strip_hostnames_from_path(path)
# Custom for /vm path - so we just collect aggrate stats for all usage along this one # Custom for /vm path - so we just collect aggrate stats for all usage along this one
# path. Custom counters are then added more specific endpoints in v1.rb # path. Custom counters are then added more specific endpoints in v1.rb
# Since we aren't parsing UID/GIDs as in the original example, these are removed. # Since we aren't parsing UID/GIDs as in the original example, these are removed.
# Similarly, request IDs are also stripped from the /ondemand path.
path path
.gsub(%r{/vm/.+$}, '/vm') .gsub(%r{/vm/.+$}, '/vm')
.gsub(%r{/ondemand/.+$}, '/ondemand') .gsub(%r{/ondemand/.+$}, '/ondemand')

View file

@ -40,7 +40,7 @@ describe Vmpooler::API::V1 do
before(:each) do before(:each) do
expect(app).to receive(:run!).once expect(app).to receive(:run!).once
app.execute([:api], config, redis, metrics) app.execute([:api], config, redis, metrics, nil)
app.settings.set :config, auth: false app.settings.set :config, auth: false
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
end end

View file

@ -43,7 +43,7 @@ describe Vmpooler::API::V1 do
before(:each) do before(:each) do
expect(app).to receive(:run!).once expect(app).to receive(:run!).once
app.execute([:api], config, redis, metrics) app.execute([:api], config, redis, metrics, nil)
app.settings.set :config, auth: false app.settings.set :config, auth: false
app.settings.set :checkoutlock, checkoutlock app.settings.set :checkoutlock, checkoutlock
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)

View file

@ -37,7 +37,7 @@ describe Vmpooler::API::V1 do
before(:each) do before(:each) do
expect(app).to receive(:run!).once expect(app).to receive(:run!).once
app.execute([:api], config, redis, metrics) app.execute([:api], config, redis, metrics, nil)
app.settings.set :config, auth: false app.settings.set :config, auth: false
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
end end

View file

@ -40,7 +40,7 @@ describe Vmpooler::API::V1 do
before(:each) do before(:each) do
expect(app).to receive(:run!).once expect(app).to receive(:run!).once
app.execute([:api], config, redis, nil) app.execute([:api], config, redis, nil, nil)
app.settings.set :config, auth: false app.settings.set :config, auth: false
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
end end

View file

@ -18,16 +18,21 @@ describe Vmpooler::API::V1 do
describe '/token' do describe '/token' do
let(:prefix) { '/api/v1' } let(:prefix) { '/api/v1' }
let(:current_time) { Time.now } let(:current_time) { Time.now }
let(:config) { { } } let(:config) { {
config: {}
} }
before(:each) do before(:each) do
expect(app).to receive(:run!).once expect(app).to receive(:run!).once
app.execute([:api], config, redis, nil) app.execute([:api], config, redis, nil, nil)
end end
describe 'GET /token' do describe 'GET /token' do
context '(auth not configured)' do context '(auth not configured)' do
let(:config) { { auth: false } } let(:config) { {
config: {},
auth: false
} }
it 'returns a 404' do it 'returns a 404' do
get "#{prefix}/token" get "#{prefix}/token"
@ -38,6 +43,7 @@ describe Vmpooler::API::V1 do
context '(auth configured)' do context '(auth configured)' do
let(:config) { let(:config) {
{ {
config: {},
auth: { auth: {
'provider' => 'dummy' 'provider' => 'dummy'
} }
@ -65,7 +71,10 @@ describe Vmpooler::API::V1 do
describe 'POST /token' do describe 'POST /token' do
context '(auth not configured)' do context '(auth not configured)' do
let(:config) { { auth: false } } let(:config) { {
config: {},
auth: false
} }
it 'returns a 404' do it 'returns a 404' do
post "#{prefix}/token" post "#{prefix}/token"
@ -76,6 +85,7 @@ describe Vmpooler::API::V1 do
context '(auth configured)' do context '(auth configured)' do
let(:config) { let(:config) {
{ {
config: {},
auth: { auth: {
'provider' => 'dummy' 'provider' => 'dummy'
} }
@ -106,7 +116,7 @@ describe Vmpooler::API::V1 do
before(:each) do before(:each) do
expect(app).to receive(:run!).once expect(app).to receive(:run!).once
app.execute([:api], config, redis, nil) app.execute([:api], config, redis, nil, nil)
app.settings.set :config, config app.settings.set :config, config
app.settings.set :redis, redis app.settings.set :redis, redis
end end
@ -118,7 +128,10 @@ describe Vmpooler::API::V1 do
describe 'GET /token/:token' do describe 'GET /token/:token' do
context '(auth not configured)' do context '(auth not configured)' do
let(:config) { { auth: false } } let(:config) { {
config: {},
auth: false
} }
it 'returns a 404' do it 'returns a 404' do
get "#{prefix}/token/this" get "#{prefix}/token/this"
@ -128,6 +141,7 @@ describe Vmpooler::API::V1 do
context '(auth configured)' do context '(auth configured)' do
let(:config) { { let(:config) { {
config: {},
auth: true, auth: true,
pools: [ pools: [
{'name' => 'pool1', 'size' => 5} {'name' => 'pool1', 'size' => 5}
@ -150,7 +164,10 @@ describe Vmpooler::API::V1 do
describe 'DELETE /token/:token' do describe 'DELETE /token/:token' do
context '(auth not configured)' do context '(auth not configured)' do
let(:config) { { auth: false } } let(:config) { {
config: {},
auth: false
} }
it 'returns a 404' do it 'returns a 404' do
delete "#{prefix}/token/this" delete "#{prefix}/token/this"
@ -161,6 +178,7 @@ describe Vmpooler::API::V1 do
context '(auth configured)' do context '(auth configured)' do
let(:config) { let(:config) {
{ {
config: {},
auth: { auth: {
'provider' => 'dummy' 'provider' => 'dummy'
} }

View file

@ -43,7 +43,7 @@ describe Vmpooler::API::V1 do
before(:each) do before(:each) do
expect(app).to receive(:run!).once expect(app).to receive(:run!).once
app.execute([:api], config, redis, metrics) app.execute([:api], config, redis, metrics, nil)
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
end end

View file

@ -40,7 +40,7 @@ describe Vmpooler::API::V1 do
before(:each) do before(:each) do
expect(app).to receive(:run!).once expect(app).to receive(:run!).once
app.execute([:api], config, redis, metrics) app.execute([:api], config, redis, metrics, nil)
app.settings.set :config, auth: false app.settings.set :config, auth: false
app.settings.set :checkoutlock, checkoutlock app.settings.set :checkoutlock, checkoutlock
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)

View file

@ -41,7 +41,7 @@ describe Vmpooler::API::V1 do
before(:each) do before(:each) do
expect(app).to receive(:run!).once expect(app).to receive(:run!).once
app.execute([:api], config, redis, metrics) app.execute([:api], config, redis, metrics, nil)
app.settings.set :config, auth: false app.settings.set :config, auth: false
app.settings.set :checkoutlock, checkoutlock app.settings.set :checkoutlock, checkoutlock
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)

View file

@ -18,6 +18,7 @@ describe Vmpooler::API do
describe 'Dashboard' do describe 'Dashboard' do
let(:config) { { let(:config) { {
config: {},
pools: [ pools: [
{'name' => 'pool1', 'size' => 5} {'name' => 'pool1', 'size' => 5}
], ],
@ -26,7 +27,7 @@ describe Vmpooler::API do
before(:each) do before(:each) do
expect(app).to receive(:run!) expect(app).to receive(:run!)
app.execute([:api], config, redis, nil) app.execute([:api], config, redis, nil, nil)
app.settings.set :config, auth: false app.settings.set :config, auth: false
end end
@ -64,6 +65,7 @@ describe Vmpooler::API do
describe '/dashboard/stats/vmpooler/pool' do describe '/dashboard/stats/vmpooler/pool' do
let(:config) { { let(:config) { {
config: {},
pools: [ pools: [
{'name' => 'pool1', 'size' => 5}, {'name' => 'pool1', 'size' => 5},
{'name' => 'pool2', 'size' => 1} {'name' => 'pool2', 'size' => 1}
@ -134,6 +136,7 @@ describe Vmpooler::API do
describe '/dashboard/stats/vmpooler/running' do describe '/dashboard/stats/vmpooler/running' do
let(:config) { { let(:config) { {
config: {},
pools: [ pools: [
{'name' => 'pool-1', 'size' => 5}, {'name' => 'pool-1', 'size' => 5},
{'name' => 'pool-2', 'size' => 1}, {'name' => 'pool-2', 'size' => 1},

View file

@ -30,6 +30,7 @@ describe 'Vmpooler' do
['experimental_features', test_bool, nil], ['experimental_features', test_bool, nil],
['purge_unconfigured_folders', test_bool, nil], ['purge_unconfigured_folders', test_bool, nil],
['usage_stats', test_bool, nil], ['usage_stats', test_bool, nil],
['request_logger', test_bool, nil],
['extra_config', test_string, nil], ['extra_config', test_string, nil],
] ]

View file

@ -509,6 +509,12 @@
# Expects a boolean value # Expects a boolean value
# (optional; default: false) # (optional; default: false)
# #
# - request_logger
# Enable API Request logging to the logger
# When enabled all requests to the API are written to the standard logger.
# Expects a boolean value
# (optional; default: false)
#
# - max_lifetime_upper_limit # - max_lifetime_upper_limit
# Sets a lifetime upper limit (in hours) for how long the vm lifetime can be set via the API. Lifetime can be set and extended # Sets a lifetime upper limit (in hours) for how long the vm lifetime can be set via the API. Lifetime can be set and extended
# so this configuration is used to enforce an upper limit to both the initial lifetime request and/or the extended # so this configuration is used to enforce an upper limit to both the initial lifetime request and/or the extended