-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into 102754_POA_notifications_migration
- Loading branch information
Showing
9 changed files
with
427 additions
and
214 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# frozen_string_literal: true | ||
|
||
module Ccra | ||
module Middleware | ||
## | ||
# Faraday middleware that logs various semantically relevant attributes needed for debugging and audit purposes | ||
# | ||
class CcraLogging < Common::Middleware::BaseLogging | ||
private | ||
|
||
# Returns the configuration for the CCRA service. | ||
# @return [Ccra::Configuration] the CCRA configuration instance. | ||
def config | ||
@config ||= Ccra::Configuration.instance | ||
end | ||
|
||
# Returns the StatsD key prefix for CCRA. | ||
# @return [String] the StatsD key prefix. | ||
def statsd_key_prefix | ||
'api.ccra.response' | ||
end | ||
end | ||
end | ||
end | ||
|
||
Faraday::Middleware.register_middleware ccra_logging: Ccra::Middleware::CcraLogging |
157 changes: 157 additions & 0 deletions
157
modules/vaos/app/services/common/middleware/base_logging.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
# frozen_string_literal: true | ||
|
||
module Common | ||
module Middleware | ||
## | ||
# Base Faraday middleware that logs various semantically relevant attributes needed for debugging and audit purposes | ||
# | ||
class BaseLogging < Faraday::Middleware | ||
# #call | ||
# | ||
# Logs all outbound request / responses to VAMF api gateway as :info when success and :warn when fail | ||
# | ||
# Semantic logging tags: | ||
# jti: The "jti" (JWT ID) claim provides a unique identifier for the JWT. | ||
# status: The HTTP status returned from upstream. | ||
# duration: The amount of time it took between request being made and response being received in seconds. | ||
# url: The HTTP Method and URL invoked in the request. | ||
# | ||
# @param env [Faraday::Env] the request/response tree | ||
# @return [Faraday::Env] | ||
def call(env) | ||
statsd_increment("#{statsd_key_prefix}.total", env) | ||
start_time = Time.current | ||
|
||
@app.call(env).on_complete do |response_env| | ||
if response_env.status.between?(200, 299) | ||
log(:info, "#{service_name} service call succeeded!", log_tags(env, start_time, response_env)) | ||
elsif response_env.status == 400 # 400 error resp at times contain PII/PHI so we don't want the err msg logged | ||
statsd_increment("#{statsd_key_prefix}.fail", env) | ||
log(:warn, "#{service_name} service call failed!", log_tags(env, start_time, response_env)) | ||
else | ||
statsd_increment("#{statsd_key_prefix}.fail", env) | ||
log(:warn, "#{service_name} service call failed!", log_error_tags(env, start_time, response_env)) | ||
end | ||
end | ||
rescue Timeout::Error, Faraday::TimeoutError, Faraday::ConnectionFailed => e | ||
statsd_increment("#{statsd_key_prefix}.fail", env, e) | ||
log(:warn, "#{service_name} service call failed - #{e.message}", log_tags(env, start_time)) | ||
raise | ||
end | ||
|
||
private | ||
|
||
# Returns the configuration for the service. Must be implemented by subclasses. | ||
# @raise [NotImplementedError] if not implemented by subclass | ||
# @return [Object] a configuration object responding to #service_name | ||
def config | ||
raise NotImplementedError, 'Subclasses must implement #config' | ||
end | ||
|
||
# Returns the service name extracted from the configuration. | ||
# @return [String] the service name | ||
def service_name | ||
config.service_name | ||
end | ||
|
||
# Returns the StatsD key prefix for the service. Must be implemented by subclasses. | ||
# @raise [NotImplementedError] if not implemented by subclass | ||
# @return [String] the StatsD key prefix | ||
def statsd_key_prefix | ||
raise NotImplementedError, 'Subclasses must implement #statsd_key_prefix' | ||
end | ||
|
||
# Builds a hash of logging tags for a request/response. | ||
# | ||
# @param env [Faraday::Env] the request environment | ||
# @param start_time [Time] the time when the request was initiated | ||
# @param response_env [Faraday::Env, nil] the response environment | ||
# @return [Hash] a hash containing logging tags | ||
def log_tags(env, start_time, response_env = nil) | ||
anon_uri = VAOS::Anonymizers.anonymize_uri_icn(env.url) | ||
{ | ||
jti: jti(env), | ||
status: response_env&.status, | ||
duration: Time.current - start_time, | ||
service_name: config.service_name, | ||
url: "(#{env.method.upcase}) #{anon_uri}" | ||
} | ||
end | ||
|
||
# Builds a hash of logging tags for error responses, including the error message. | ||
# | ||
# @param env [Faraday::Env] the request environment | ||
# @param start_time [Time] the time when the request was initiated | ||
# @param response_env [Faraday::Env] the response environment | ||
# @return [Hash] a hash containing logging tags with error info | ||
def log_error_tags(env, start_time, response_env) | ||
tags = log_tags(env, start_time, response_env) | ||
tags.merge(vamf_msg: response_env&.body) | ||
end | ||
|
||
# Increments a StatsD metric for the given key using request details. | ||
# | ||
# @param key [String] the StatsD metric key | ||
# @param env [Faraday::Env] the request environment | ||
# @param error [Exception, nil] an optional error object | ||
# @return [void] | ||
def statsd_increment(key, env, error = nil) | ||
StatsDMetric.new(key:).save | ||
tags = [ | ||
"method:#{env.method.upcase}", | ||
"url:#{StringHelpers.filtered_endpoint_tag(env.url.path)}", | ||
"http_status:#{error.present? ? error.class : env.status}" | ||
] | ||
StatsD.increment(key, tags:) | ||
end | ||
|
||
# Logs a message using Rails.logger with the given type and tags. | ||
# | ||
# @param type [Symbol] one of [:info, :warn] | ||
# @param message [String] the string you would like to appear in logs | ||
# @param tags [Hash] key value pairs of semantically relevant tags needed for debugging | ||
# @return [Boolean] returns true or false | ||
def log(type, message, tags) | ||
Rails.logger.send(type, message, **tags) | ||
end | ||
|
||
# Decodes a JWT token without verifying its signature. | ||
# | ||
# @param token [String] The JWT token received in the response | ||
# @return [Hash] returns a JSON Hash object corresponding to JWT specification | ||
def decode_jwt_no_sig_check(token) | ||
JWT.decode(token, nil, false).first | ||
end | ||
|
||
# Extracts the "jti" (JWT ID) claim from the request or response. | ||
# | ||
# @param env [Faraday::Env] The Request/Response tree object | ||
# @return [String] The JTI value or "unknown jti" if a parsing or other error is encountered (failing gracefully) | ||
def jti(env) | ||
if user_session_request?(env) | ||
decode_jwt_no_sig_check(env.body)['jti'] | ||
else | ||
decode_jwt_no_sig_check(x_vamf_headers(env.request_headers))['jti'] | ||
end | ||
rescue | ||
'unknown jti' | ||
end | ||
|
||
# Determines if the current request is a user session request. | ||
# | ||
# @param env [Faraday::Env] the request/response environment | ||
# @return [Boolean] true if it is a user session request, false otherwise | ||
def user_session_request?(env) | ||
env.url.to_s.include?('users/v2/session?processRules=true') | ||
end | ||
|
||
# Extracts the JWT from the X-Vamf-Header in the request headers. | ||
# | ||
# @param request_headers The set of request headers | ||
# @return [String] the JWT set in the request headers | ||
def x_vamf_headers(request_headers) | ||
request_headers['X-Vamf-Jwt'] || request_headers['X-VAMF-JWT'] | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
modules/vaos/spec/services/ccra/middleware/ccra_logging_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rails_helper' | ||
require_relative '../../../support/fixture_helper' | ||
|
||
describe Ccra::Middleware::CcraLogging do | ||
subject(:client) do | ||
Faraday.new do |conn| | ||
conn.use :ccra_logging | ||
|
||
conn.adapter :test do |stub| | ||
stub.get(test_uri) { [status, { 'Content-Type' => 'text/plain', 'X-Vamf-Jwt' => sample_jwt }, '{}'] } | ||
end | ||
end | ||
end | ||
|
||
let(:sample_jwt) { read_fixture_file('sample_jwt.response') } | ||
let(:test_uri) { 'https://fake.ccra/api' } | ||
let(:status) { 200 } | ||
|
||
it 'uses correct configuration' do | ||
expect(Ccra::Configuration.instance).to receive(:service_name).at_least(:once).and_return('CCRA') | ||
client.get(test_uri, nil, { 'X-Vamf-Jwt' => sample_jwt }) | ||
end | ||
|
||
it 'uses correct statsd prefix' do | ||
expect { client.get(test_uri, nil, { 'X-Vamf-Jwt' => sample_jwt }) } | ||
.to trigger_statsd_increment( | ||
'api.ccra.response.total', | ||
tags: ['method:GET', 'url:/api', 'http_status:'] | ||
) | ||
end | ||
end |
Oops, something went wrong.