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 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