svcbase 0.1.16

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
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
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.0
5
+ before_install: gem install bundler -v 1.15.4
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in svcbase.gemspec
8
+ gemspec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ ---
2
+ WORKER_SHUTDOWN_GRACE_PERIOD: 15 # Seconds to wait for a worker to finish when shutting down
3
+ STAT_LOG_SECONDS: 3600 # How often to log endpoint statistics
4
+ THRESHOLDER_SECONDS: 60 # Default thresholder window
5
+ THRESHOLDER_COUNT: 60 # Default thresholder count
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