svcbase 0.1.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +63 -0
- data/.rubocop_todo.yml +7 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +8 -0
- data/Jenkinsfile +137 -0
- data/README.md +61 -0
- data/Rakefile +17 -0
- data/bin/console +16 -0
- data/bin/setup +8 -0
- data/config/configs/base.yml +5 -0
- data/config/configs/local.yml +0 -0
- data/lib/svcbase/api/base.rb +73 -0
- data/lib/svcbase/api/etag.rb +11 -0
- data/lib/svcbase/api/requesthelpers.rb +34 -0
- data/lib/svcbase/appversion.rb +15 -0
- data/lib/svcbase/config/config_helper.rb +49 -0
- data/lib/svcbase/config.rb +50 -0
- data/lib/svcbase/corelogger.rb +52 -0
- data/lib/svcbase/dumpstats.rb +27 -0
- data/lib/svcbase/exceptions.rb +170 -0
- data/lib/svcbase/formatter.rb +90 -0
- data/lib/svcbase/ipaddr_helper.rb +34 -0
- data/lib/svcbase/middleware/apilogger.rb +184 -0
- data/lib/svcbase/middleware/dateheader.rb +16 -0
- data/lib/svcbase/middleware/requestid.rb +21 -0
- data/lib/svcbase/random.rb +31 -0
- data/lib/svcbase/server.rb +58 -0
- data/lib/svcbase/stats.rb +85 -0
- data/lib/svcbase/thresholder.rb +124 -0
- data/lib/svcbase/version.rb +5 -0
- data/lib/svcbase/worker.rb +66 -0
- data/lib/svcbase.rb +14 -0
- data/locale/en.yml +28 -0
- data/locale/zz.yml +21 -0
- data/svcbase.gemspec +51 -0
- metadata +305 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ee9043e3f2173b8d9d6adfe661dd37f7db925a00912f9c9a15e7f74dede6ea74
|
4
|
+
data.tar.gz: 1be47052db3c376410cee00d62d16997b46eaf31e317a9010fdc34f52eb68199
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ad56907c1f5572d9677280f743b18f04f1011b4556c5b2b1f658ff975ea9006d677e20e30d72d75017373f4a41b3cb8d4c3d383a48d70e01fd44fc655f3c5a49
|
7
|
+
data.tar.gz: 1fce1b726d8507e00072a976cfb227321389f80fd55d5ecbe1d50d578c3f0dfc1933ca6d1904b07db2ced7ad976fb489e43e651c0347d66f455cf6bd7f038d55
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.5.1
|
3
|
+
Exclude:
|
4
|
+
- 'test/*'
|
5
|
+
- 'vendor/**/*'
|
6
|
+
|
7
|
+
inherit_from: .rubocop_todo.yml
|
8
|
+
|
9
|
+
Style/AccessModifierDeclarations:
|
10
|
+
EnforcedStyle: inline
|
11
|
+
|
12
|
+
Metrics/CyclomaticComplexity:
|
13
|
+
Max: 12
|
14
|
+
|
15
|
+
Metrics/PerceivedComplexity:
|
16
|
+
Max: 12
|
17
|
+
|
18
|
+
# Grape idiom for defining APIs nearly guarantees long blocks,
|
19
|
+
# so exclude our entire api subdir from this cop.
|
20
|
+
Metrics/BlockLength:
|
21
|
+
Exclude:
|
22
|
+
- 'spec/**/*'
|
23
|
+
|
24
|
+
Metrics/AbcSize:
|
25
|
+
Max: 30
|
26
|
+
Exclude:
|
27
|
+
- 'spec/**/*'
|
28
|
+
|
29
|
+
Metrics/ClassLength:
|
30
|
+
Max: 125
|
31
|
+
Exclude:
|
32
|
+
- 'spec/**/*'
|
33
|
+
|
34
|
+
Metrics/ModuleLength:
|
35
|
+
Max: 125
|
36
|
+
Exclude:
|
37
|
+
- 'spec/**/*'
|
38
|
+
|
39
|
+
Metrics/LineLength:
|
40
|
+
Max: 115
|
41
|
+
|
42
|
+
Metrics/MethodLength:
|
43
|
+
Max: 20
|
44
|
+
|
45
|
+
Metrics/ParameterLists:
|
46
|
+
CountKeywordArgs: false
|
47
|
+
|
48
|
+
Layout/LeadingCommentSpace:
|
49
|
+
Exclude:
|
50
|
+
- 'config.ru'
|
51
|
+
|
52
|
+
Style/MixinGrouping:
|
53
|
+
Exclude:
|
54
|
+
- 'spec/**/*'
|
55
|
+
|
56
|
+
Naming/UncommunicativeMethodParamName:
|
57
|
+
Exclude:
|
58
|
+
- 'spec/**/*'
|
59
|
+
AllowedNames:
|
60
|
+
- ip
|
61
|
+
- id
|
62
|
+
- tz
|
63
|
+
- db
|
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2017-03-31 12:03:15 -0400 using RuboCop version 0.48.0.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.5.1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Jenkinsfile
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
#!/usr/bin/env groovy
|
2
|
+
|
3
|
+
// load helpers
|
4
|
+
library 'jenkins-pipeline-libs'
|
5
|
+
version = null
|
6
|
+
is_master = ( "${env.BRANCH_NAME}" == "master" )
|
7
|
+
|
8
|
+
node('docker && !windows') {
|
9
|
+
String gh_cid = scm.getUserRemoteConfigs()[0].getCredentialsId()
|
10
|
+
String scmUrl = scm.getUserRemoteConfigs()[0].getUrl()
|
11
|
+
String repo_name = getRepoFromURL(scmUrl)
|
12
|
+
|
13
|
+
properties([
|
14
|
+
|
15
|
+
// dont keep builds in Jenkins.
|
16
|
+
buildDiscarder( logRotator(artifactDaysToKeepStr: '',
|
17
|
+
artifactNumToKeepStr: '3',
|
18
|
+
daysToKeepStr: '',
|
19
|
+
numToKeepStr: '30') ),
|
20
|
+
|
21
|
+
// set url for diff links to gh
|
22
|
+
[ $class: 'GithubProjectProperty',
|
23
|
+
displayName: '',
|
24
|
+
projectUrlStr: "${getGitHubURL(scmUrl)}" ]
|
25
|
+
])
|
26
|
+
|
27
|
+
try {
|
28
|
+
stage('Checkout') {
|
29
|
+
deleteDir()
|
30
|
+
checkout scm
|
31
|
+
}
|
32
|
+
|
33
|
+
//invoke build steps
|
34
|
+
stage('Builds') {
|
35
|
+
//set display name
|
36
|
+
currentBuild.displayName = getVersion()
|
37
|
+
|
38
|
+
ansiColor('xterm') {
|
39
|
+
sh 'gem build svcbase.gemspec'
|
40
|
+
} // ansiColor
|
41
|
+
} // stage
|
42
|
+
//unit tests
|
43
|
+
|
44
|
+
stage('Unit Tests') {
|
45
|
+
String packages = "tzdata ruby-dev zlib-dev xz-dev build-base libxml2-dev libxslt-dev git"
|
46
|
+
String env_cmd = "apk update && apk upgrade && apk add ${packages}"
|
47
|
+
String test_cmd = "gem install bundler; cd /usr/src/app; bundle install --with development test; bundle exec rake"
|
48
|
+
String full_cmd = "${env_cmd}; ${test_cmd}"
|
49
|
+
String test_container = "svcbase-test-${env.BRANCH_NAME}-${env.BUILD_NUMBER}"
|
50
|
+
String docker_img = "ruby:2.5.1-alpine"
|
51
|
+
String container_dir = "/usr/src/app"
|
52
|
+
|
53
|
+
sh """
|
54
|
+
#!/bin/bash -le
|
55
|
+
docker pull ${docker_img}
|
56
|
+
docker run -d -it --name ${test_container} ${docker_img}
|
57
|
+
docker cp $WORKSPACE ${test_container}:${container_dir}
|
58
|
+
docker exec ${test_container} ash -c "${env_cmd}"
|
59
|
+
docker exec ${test_container} ash -c "${test_cmd}"
|
60
|
+
# copy results back
|
61
|
+
docker cp ${test_container}:/usr/src/app/coverage .
|
62
|
+
# now stop container and image
|
63
|
+
docker stop ${test_container}
|
64
|
+
docker rm ${test_container}
|
65
|
+
"""
|
66
|
+
|
67
|
+
// publish ut results
|
68
|
+
publishHTML([ allowMissing: false,
|
69
|
+
alwaysLinkToLastBuild: true,
|
70
|
+
keepAll: true,
|
71
|
+
reportDir: 'coverage',
|
72
|
+
reportFiles: 'index.html',
|
73
|
+
reportName: 'RCov Report'
|
74
|
+
])
|
75
|
+
} //stage
|
76
|
+
|
77
|
+
// invoke any steps specific to the master(release) branch
|
78
|
+
if ( is_master ) {
|
79
|
+
stage('Tag & Publish') {
|
80
|
+
withCredentials([usernameColonPassword(credentialsId: 'bf127b02-43c5-4ca9-8523-c2f22372ea7a', variable: 'ART_KEY')]) {
|
81
|
+
String art_url = "https://artifactory.secureauth.com/artifactory/api/gems/rubygems-local"
|
82
|
+
|
83
|
+
// set creds and push to artifactory (is there a better way :/ )
|
84
|
+
sh """
|
85
|
+
mkdir -p ~/.gem
|
86
|
+
curl -s -u${ART_KEY} ${art_url}/api/v1/api_key.yaml > ~/.gem/credentials
|
87
|
+
chmod 600 ~/.gem/credentials
|
88
|
+
gem push svcbase-${getVersion()}.gem --host ${art_url}
|
89
|
+
"""
|
90
|
+
}
|
91
|
+
applyTag(gh_cid, "${getVersion()}", scmUrl)
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
catch (e) {
|
96
|
+
// set status to failed
|
97
|
+
currentBuild.result = "FAILED"
|
98
|
+
throw e
|
99
|
+
}
|
100
|
+
finally {
|
101
|
+
|
102
|
+
// wrap it up
|
103
|
+
stage('Archive, Clean & Notify') {
|
104
|
+
// remove credentials
|
105
|
+
sh "rm -f ~/.gem/credentials"
|
106
|
+
|
107
|
+
String recipients = '#cloudteam_notify'
|
108
|
+
String status = currentBuild.result ?: 'SUCCESS'
|
109
|
+
String msg = "${env.JOB_NAME} - <${env.BUILD_URL}|${getVersion()}" +
|
110
|
+
"> - ${status}\n\n${getChangeString()}"
|
111
|
+
|
112
|
+
//send via library
|
113
|
+
notifySlack {
|
114
|
+
buildStatus = status
|
115
|
+
channel = recipients
|
116
|
+
message = msg
|
117
|
+
}
|
118
|
+
|
119
|
+
deleteDir()
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
// note - must be called after scm checkout
|
125
|
+
String getVersion(){
|
126
|
+
if (!version) {
|
127
|
+
if ( is_master ) {
|
128
|
+
def matches = readFile('lib/svcbase/version.rb') =~ /VERSION *= *['"]?([0-9\.]+)['"]?/
|
129
|
+
version = matches ? matches[0][1] : null
|
130
|
+
} else {
|
131
|
+
version = "${env.BUILD_NUMBER}"
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
return version
|
136
|
+
}
|
137
|
+
|
data/README.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# Svcbase
|
2
|
+
|
3
|
+
This is a base class for Grape apps following our API methodology.
|
4
|
+
|
5
|
+
It includes
|
6
|
+
|
7
|
+
* API Logging, including sensitive data filtering and periodic stat output
|
8
|
+
* Configuration (file) support
|
9
|
+
* Locale support
|
10
|
+
* API request helpers for common data
|
11
|
+
* Request ID tracking
|
12
|
+
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'svcbase'
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
$ bundle
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
$ gem install svcbase
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
You can reference the entire stack by simply doing a
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
require 'svcbase'
|
36
|
+
```
|
37
|
+
but it is recommended that only the relevant parts are required. For example, to start the behind-the-scenes thread server, simply
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
require 'svcbase/server'
|
41
|
+
```
|
42
|
+
and then later reference ```
|
43
|
+
Core::Server.
|
44
|
+
```
|
45
|
+
|
46
|
+
To create a new top-level API, first
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
require 'svcbase/api/base'
|
50
|
+
```
|
51
|
+
and then create a class that inherits from ```
|
52
|
+
Core::APIBase
|
53
|
+
```
|
54
|
+
|
55
|
+
## Development
|
56
|
+
|
57
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
58
|
+
|
59
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
60
|
+
|
61
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec, %i[dir file]) do |spec, args|
|
7
|
+
args.with_defaults dir: '*', file: '*_spec.rb'
|
8
|
+
spec.pattern = "spec/#{args[:dir]}/**/#{args[:file]}"
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'rubocop/rake_task'
|
12
|
+
RuboCop::RakeTask.new(:rubocop) do |task|
|
13
|
+
task.options << '--display-cop-names'
|
14
|
+
task.options << '--parallel'
|
15
|
+
end
|
16
|
+
|
17
|
+
task default: %i[rubocop spec]
|
data/bin/console
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'bundler/setup'
|
6
|
+
require 'svcbase'
|
7
|
+
|
8
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
9
|
+
# with your gem easier. You can also use a different console, if you like.
|
10
|
+
|
11
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
12
|
+
# require 'pry'
|
13
|
+
# Pry.start
|
14
|
+
|
15
|
+
require 'irb'
|
16
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
File without changes
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'grape'
|
4
|
+
require 'svcbase/exceptions'
|
5
|
+
require 'http_accept_language'
|
6
|
+
|
7
|
+
require 'i18n'
|
8
|
+
|
9
|
+
require 'svcbase/api/requesthelpers'
|
10
|
+
|
11
|
+
require 'svcbase/corelogger'
|
12
|
+
require 'svcbase/middleware/dateheader'
|
13
|
+
require 'svcbase/middleware/apilogger'
|
14
|
+
require 'svcbase/middleware/requestid'
|
15
|
+
|
16
|
+
module Core
|
17
|
+
# API container
|
18
|
+
class APIBase < Grape::API
|
19
|
+
format :json
|
20
|
+
rescue_from Grape::Exceptions::Base do |e|
|
21
|
+
# grape uses 'error' but we want 'message', so translate
|
22
|
+
error!({ status: :error, error: { code: :grape, message: e.message } }, e.status, e.headers || {})
|
23
|
+
end
|
24
|
+
rescue_from Core::Exceptions::Error do |e|
|
25
|
+
# our errors give us a method to call to get the response
|
26
|
+
error!(e.response, e.status, e.headers)
|
27
|
+
end
|
28
|
+
rescue_from :all do |e|
|
29
|
+
# anything that gets here is, by definition, unexpected.
|
30
|
+
# log the error details at FATAL, but don't return them to the caller.
|
31
|
+
# instead, give them a generic 500 error
|
32
|
+
log.fatal e
|
33
|
+
error!({ status: :error, error: { code: :internal_server_error } }, 500)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set up translations for our messages in addition to whatever else might be set up
|
37
|
+
I18n.enforce_available_locales ||= false
|
38
|
+
I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
|
39
|
+
I18n.default_locale ||= :en
|
40
|
+
I18n.load_path << Dir.glob(File.join(File.dirname(__FILE__), '../../../locale', '*.{rb,yml}'))
|
41
|
+
|
42
|
+
use Core::AddDateHeader
|
43
|
+
use RequestStore::Middleware
|
44
|
+
use Core::RequestId
|
45
|
+
use Core::ApiLogger, logger: Core::Logger.instance, filter: Core::ParamFilter.new
|
46
|
+
use HttpAcceptLanguage::Middleware
|
47
|
+
do_not_route_head!
|
48
|
+
do_not_route_options!
|
49
|
+
|
50
|
+
helpers RequestHelpers
|
51
|
+
|
52
|
+
# restrict payload content-type
|
53
|
+
# Although grape provides "format :json", that doesn't stop rack from handling
|
54
|
+
# certain types before grape even gets involved (x-www-form-urlencoded, etc.)
|
55
|
+
# This will enforce the rule, regardless.
|
56
|
+
before do
|
57
|
+
# set the language for this Thread based on request headers
|
58
|
+
I18n.locale = accept_language unless accept_language.nil?
|
59
|
+
|
60
|
+
# authenticate!
|
61
|
+
next unless env && env['CONTENT_LENGTH'] && env['CONTENT_LENGTH'] != '0'
|
62
|
+
# NB. would be nice if we could figure out how to translate the declared format
|
63
|
+
# which lives in env[Grape::Env::API_FORMAT] into a content-type so we don't
|
64
|
+
# have to hardcode application/json and we don't have to add stuff to the
|
65
|
+
# endpoint description.
|
66
|
+
allowed_content_types = Array(route_desc[:allowed_content_types])
|
67
|
+
allowed_content_types = ['application/json'] if allowed_content_types.empty?
|
68
|
+
unless allowed_content_types.include?(env['CONTENT_TYPE'])
|
69
|
+
raise Core::Exceptions::UnsupportedMediaType, msg: :invalid, msgobj: :content_type
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# compute etag hashes based on ID and updated_at of all items passed
|
4
|
+
module Etag
|
5
|
+
def self.weak_hash(*args)
|
6
|
+
val = args.flatten.compact.map do |item|
|
7
|
+
item.id.to_s + item.updated_at.to_i.to_s
|
8
|
+
end.join
|
9
|
+
"W/\"#{val.to_i.to_s(32)}\""
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'request_store'
|
4
|
+
|
5
|
+
# request helpers
|
6
|
+
module RequestHelpers
|
7
|
+
extend Grape::API::Helpers
|
8
|
+
|
9
|
+
def http_request_id
|
10
|
+
RequestStore.store[:http_request_id]
|
11
|
+
end
|
12
|
+
|
13
|
+
def input_data
|
14
|
+
env[Grape::Env::API_REQUEST_BODY]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get accept-language
|
18
|
+
def accept_language
|
19
|
+
available = ::LOCALE_LIST if defined? ::LOCALE_LIST
|
20
|
+
env.http_accept_language.compatible_language_from(available)
|
21
|
+
end
|
22
|
+
|
23
|
+
def client_ip
|
24
|
+
request.env['HTTP_X_FORWARDED_FOR'] || request.env['REMOTE_ADDR'] || '0.0.0.0'
|
25
|
+
end
|
26
|
+
|
27
|
+
def user_agent
|
28
|
+
request.env['HTTP_USER_AGENT'] || 'unknown'
|
29
|
+
end
|
30
|
+
|
31
|
+
def route_desc
|
32
|
+
route_setting(:description) || {}
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Core
|
4
|
+
# version info of application (from metadata)
|
5
|
+
class Version
|
6
|
+
private_class_method def self.safe_set(name, file)
|
7
|
+
const_set name, File.read(file).strip
|
8
|
+
rescue StandardError
|
9
|
+
const_set name, "no #{name}"
|
10
|
+
end
|
11
|
+
|
12
|
+
safe_set :ID, './git-commit'
|
13
|
+
safe_set :TAG, './git-tag'
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# helpers module to DRY out config fetches with optional default
|
4
|
+
#
|
5
|
+
# - iterates through args looking for first symbol that is present.
|
6
|
+
# - non-symbols are treated as literals instead of lookups (so only one matters)
|
7
|
+
# - raises an error if value is present but not valid for type (Integer or Float)
|
8
|
+
# - raises an error if value is missing and no default specified
|
9
|
+
# - if supplied, default must either be valid for type or nil
|
10
|
+
module ConfigHelper
|
11
|
+
def get_x!(*args)
|
12
|
+
while args.length.positive?
|
13
|
+
k = args.shift
|
14
|
+
if k.is_a? Symbol
|
15
|
+
k = k.to_s
|
16
|
+
return yield(self[k]) if key? k
|
17
|
+
else
|
18
|
+
raise 'default not last argument' unless args.empty?
|
19
|
+
return nil if k.nil?
|
20
|
+
return yield(k)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
raise 'value not found'
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_i!(*args)
|
27
|
+
get_x!(*args) { |v| Integer(v) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_f!(*args)
|
31
|
+
get_x!(*args) { |v| Float(v) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_s!(*args)
|
35
|
+
get_x!(*args, &:to_s)
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_b!(*args)
|
39
|
+
get_x!(*args) do |v|
|
40
|
+
return v if [true, false].include?(v)
|
41
|
+
raise ArgumentError, 'not a bool or string' unless v.is_a? String
|
42
|
+
case v.downcase
|
43
|
+
when 'true' then true
|
44
|
+
when 'false' then false
|
45
|
+
else raise ArgumentError, 'not true or false'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/hash/deep_merge'
|
4
|
+
require 'forwardable'
|
5
|
+
require 'pathname'
|
6
|
+
require 'singleton'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
require_relative 'config/config_helper'
|
10
|
+
|
11
|
+
# Global config wrapper
|
12
|
+
module Core
|
13
|
+
# configuration wrapper
|
14
|
+
class Config
|
15
|
+
include Singleton
|
16
|
+
include ConfigHelper
|
17
|
+
|
18
|
+
attr_reader :config_name
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@config_name = ENV['SA_ENV'] ||= 'local'
|
22
|
+
@config = {}
|
23
|
+
['base', @config_name].each do |name|
|
24
|
+
f = Pathname.new(CONFIG_DIR).join("#{name}.yml")
|
25
|
+
c = YAML.load_file(f)
|
26
|
+
next unless c
|
27
|
+
@config.deep_merge!(c)
|
28
|
+
rescue StandardError => e
|
29
|
+
STDERR.puts "Error reading config file #{e}"
|
30
|
+
raise
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def key?(key)
|
35
|
+
ENV.key?(key) || @config.key?(key)
|
36
|
+
end
|
37
|
+
|
38
|
+
def [](key)
|
39
|
+
ENV.key?(key) ? ENV[key] : @config[key]
|
40
|
+
end
|
41
|
+
|
42
|
+
# forward class method calls to the instance so we can do Config['VARNAME'] or Config.get_i!(:FOO)
|
43
|
+
class << self
|
44
|
+
extend Forwardable
|
45
|
+
def_delegators :instance, *Config.instance_methods(false), *ConfigHelper.instance_methods(false)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Core::Config.instance
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'singleton'
|
5
|
+
require 'svcbase/formatter'
|
6
|
+
|
7
|
+
# define our own custom logger
|
8
|
+
module Core
|
9
|
+
# our logger
|
10
|
+
class Logger < ::Logger
|
11
|
+
include Singleton
|
12
|
+
def initialize
|
13
|
+
super($stdout)
|
14
|
+
self.formatter = Core::Formatters::Log.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def none(*_unused)
|
18
|
+
# intentional no-op. Sequel will log things at a defined log level
|
19
|
+
# but we want to be able to log long-running queries without logging
|
20
|
+
# ALL of them. this gives us a method to /dev/zero messages for fast
|
21
|
+
# queries
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# truncate long parameters, mask password
|
26
|
+
class ParamFilter
|
27
|
+
def filter(paramhash, maskkeys)
|
28
|
+
paramhash.each do |key, value|
|
29
|
+
next paramhash[key] = '*' if maskkeys&.include?(key.to_sym)
|
30
|
+
next
|
31
|
+
|
32
|
+
case value
|
33
|
+
when String
|
34
|
+
paramhash[key] = "#{value[1..128]}... (#{value.length})" if value.length > 128
|
35
|
+
when Hash, Array
|
36
|
+
value = value.to_json.to_s
|
37
|
+
paramhash[key] = "#{value[1..128]}... (#{value.length})" if value.length > 128
|
38
|
+
end
|
39
|
+
end
|
40
|
+
paramhash
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# add a logger method to all objects so that we can access our logger anywhere
|
46
|
+
class Object
|
47
|
+
def log
|
48
|
+
Core::Logger.instance
|
49
|
+
end
|
50
|
+
|
51
|
+
alias logger log
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'svcbase/stats'
|
5
|
+
require 'svcbase/worker'
|
6
|
+
|
7
|
+
module Core
|
8
|
+
module Workers
|
9
|
+
# Stats collector
|
10
|
+
class DumpStats
|
11
|
+
include Worker
|
12
|
+
include Singleton
|
13
|
+
|
14
|
+
private def run_at_startup
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
private def interval_seconds
|
19
|
+
Config.get_f!(:STAT_LOG_SECONDS, 3600)
|
20
|
+
end
|
21
|
+
|
22
|
+
private def do_work
|
23
|
+
Core::Stats.log_and_reset
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|