svcbase 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|