hoodoo 1.5.2 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ODRjNTI3Y2YyMGU1NThkYTVlNmI0YzZjMzJiNzE3MDg3NGU1Zjg4OQ==
4
+ MmIwZDk4ODJmMWM2MGQ0MjIwNWMwYTRjZTZlZjE3NDM5NWUxOTgzYg==
5
5
  data.tar.gz: !binary |-
6
- ZTljMjA0NWU4ZTY0NjU4OGNiNTg5NDBiNzU5OWU4ZTA2MmYwNzJjNQ==
6
+ MDA4ODMwYjc2Zjc1MTQ1Njk0YzMxNzRiMTFjMDRkYTdmMTI2MTA3YQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- YWI1MDQwNzgzMjVmMTExM2JmM2QwYTI2YmI1OWU1ZjM2ZmE3ZWVkZWViMmE0
10
- MzBiN2MxYThiYzNlYjcyY2ZlNmY0YmEzNzg4NjIwMDQyYWQ5ZmUwOWMzZWJj
11
- YjkxMGNjNDA2MjdmM2FhODg0OWZkNzUwOGJkNmIyNTQ5YjQ0N2U=
9
+ MzRlYzJiNDcyMjcxNzg1NWI0MGU2Y2IyMTFmZTI5Y2I4NTkxNGE1YmZkZWM2
10
+ ZWM4MmZhNzZkNjNhZmY0ZDM5Mjg5Mjg4NDgzYTM0YzExNDAwNDk0MDkwODIz
11
+ MThkNGYwMWM3Njg3ZGQwNmIyMjM3ZjcxMzg5MDU2YzZiMzk0NmE=
12
12
  data.tar.gz: !binary |-
13
- YzUzY2M0M2U4OWMyNWY1ZGQzYjU5YjAwM2EwZjY2OTU4ZDA0NTc1OThmZDI2
14
- ODVhMTFjZDkwYTEwOTZhYzk1Zjk5MzAyMzRlY2FjOTlmZmI0NGIxZDAwN2E5
15
- MmIwNDMwYTNiZDY5NTc5YjgzNzA1OTk4ZTQ2ZmZmMzNlZDJmMjA=
13
+ YWViZTVjNDRkZTM1ZTYzZmRlYjg5YmU2YThjYTNjN2RhYTAyZGNmNjQxZmY2
14
+ NDQ3ZDM2MmVmMGRmMzg2YWI1MTk5NDJlNWVkZWM2ZmNlNDZmYmM5NjUwMjYw
15
+ MzI1MzAxMDQ4MTMwY2JmN2NiNTI1ZTk0MmM1NTE2ZTU2NGU4ZTE=
@@ -103,13 +103,17 @@ module Hoodoo
103
103
  # Internal default timeouts may even mean that the writer is still running
104
104
  # (possibly entirely hung).
105
105
  #
106
- # +writer_instance+:: An _instance_ of a subclass of
107
- # Hoodoo::Logger::FastWriter or
108
- # Hoodoo::Logger::SlowWriter.
106
+ # +writer_instances+:: One or more _instances_ of a subclass of
107
+ # Hoodoo::Logger::FastWriter or
108
+ # Hoodoo::Logger::SlowWriter, passed as one or
109
+ # more comma-separated parameters.
109
110
  #
110
- def remove( writer_instance )
111
- communicator = @writers[ writer_instance ]
112
- @pool.remove( communicator ) unless communicator.nil?
111
+ def remove( *writer_instances )
112
+ writer_instances.each do | writer_instance |
113
+ communicator = @writers[ writer_instance ]
114
+ @pool.remove( communicator ) unless communicator.nil?
115
+ @writers.delete( writer_instance )
116
+ end
113
117
  end
114
118
 
115
119
  # Remove all writer instances from this logger.
@@ -124,6 +128,40 @@ module Hoodoo
124
128
  @writers = {}
125
129
  end
126
130
 
131
+ # Does this log instance's collection of writers include the given writer
132
+ # instance? Returns +true+ if so, else +false+.
133
+ #
134
+ # +writer_instance+:: An _instance_ of a subclass of
135
+ # Hoodoo::Logger::FastWriter or
136
+ # Hoodoo::Logger::SlowWriter.
137
+ #
138
+ def include?( writer_instance )
139
+ @writers.has_key?( writer_instance )
140
+ end
141
+
142
+ alias_method( :includes?, :include? )
143
+
144
+ # Does this log instance's collection of writers include any writer
145
+ # instances which are of the given writer _class_? Returns +true+ if so,
146
+ # else +false+.
147
+ #
148
+ # This is slower than #include? so try to work with writer instance
149
+ # queries rather than writer class queries if you can.
150
+ #
151
+ # +writer_class+:: A _subclass_ (class reference, not instance) of
152
+ # Hoodoo::Logger::FastWriter or
153
+ # Hoodoo::Logger::SlowWriter.
154
+ #
155
+ def include_class?( writer_class )
156
+ @writers.keys.each do | writer_instance |
157
+ return true if writer_instance.is_a?( writer_class )
158
+ end
159
+
160
+ return false
161
+ end
162
+
163
+ alias_method( :includes_class?, :include_class? )
164
+
127
165
  # Returns an array of all log writer instances currently in use, in order
128
166
  # of addition. See #add.
129
167
  #
@@ -65,25 +65,22 @@ module Hoodoo; module Services
65
65
  def report( level, component, code, data )
66
66
  return if @alchemy.nil?
67
67
 
68
- # Take care with Symbol keys in 'data' vs string keys in e.g. 'Session'.
69
-
70
- data[ :id ] ||= Hoodoo::UUID.generate()
68
+ # Take care with Symbol keys in 'data' vs string keys in e.g. 'session'.
71
69
 
72
70
  session = data[ :session ] || {}
73
71
  message = {
72
+ :id => data[ :id ],
73
+ :level => level,
74
+ :component => component,
75
+ :code => code,
76
+ :reported_at => Time.now.iso8601( 12 ),
74
77
 
75
- :id => data[ :id ],
76
- :level => level,
77
- :component => component,
78
- :code => code,
79
- :reported_at => Time.now.iso8601( 12 ),
80
-
81
- :data => data,
82
-
83
- :interaction_id => data[ :interaction_id ],
84
- :caller_id => session[ 'caller_id' ],
85
- :identity => ( session[ 'identity' ] || {} ).to_h
78
+ :interaction_id => data[ :interaction_id ],
79
+ :data => data,
86
80
 
81
+ :caller_id => session[ 'caller_id' ],
82
+ :caller_identity_name => session[ 'caller_identity_name' ],
83
+ :identity => ( session[ 'identity' ] || {} ).to_h
87
84
  }.to_json()
88
85
 
89
86
  @alchemy.send_message_to_service( @routing_key, { "body" => message } )
@@ -159,15 +159,16 @@ module Hoodoo; module Services
159
159
  # middleware for simplicity/speed at parse time.
160
160
  #
161
161
  DEFAULT_TEST_SESSION.from_h!( {
162
- 'session_id' => '01234567890123456789012345678901',
163
- 'expires_at' => ( Time.now + 172800 ).utc.iso8601,
164
- 'caller_version' => 1,
165
- 'caller_id' => 'c5ea12fb7f414a46850e73ee1bf6d95e',
166
- 'identity' => { 'caller_id' => 'c5ea12fb7f414a46850e73ee1bf6d95e' },
167
- 'permissions' => Hoodoo::Services::Permissions.new( {
162
+ 'session_id' => '01234567890123456789012345678901',
163
+ 'expires_at' => ( Time.now + 172800 ).utc.iso8601,
164
+ 'caller_version' => 1,
165
+ 'caller_id' => 'c5ea12fb7f414a46850e73ee1bf6d95e',
166
+ 'caller_identity_name' => 'c5ea12fb7f414a46850e73ee1bf6d95e',
167
+ 'identity' => { 'caller_id' => 'c5ea12fb7f414a46850e73ee1bf6d95e' },
168
+ 'permissions' => Hoodoo::Services::Permissions.new( {
168
169
  'default' => { 'else' => Hoodoo::Services::Permissions::ALLOW }
169
170
  } ).to_h,
170
- 'scoping' => {
171
+ 'scoping' => {
171
172
  'authorised_http_headers' => Hoodoo::Client::Headers::HEADER_TO_PROPERTY.map() { | key, sub_hash |
172
173
  sub_hash[ :header ] if sub_hash[ :secured ] == true
173
174
  }.compact
@@ -322,6 +323,29 @@ module Hoodoo; module Services
322
323
  self.send( :add_file_logging, base_path )
323
324
  end
324
325
 
326
+ # Set verbose logging. With verbose logging enabled, additional payload
327
+ # data is added - most notably, full session details (where possible)
328
+ # are included in each log message. These can increase log data size
329
+ # considerably, but may be useful if you encounter session-related
330
+ # errors or general operational issues and need log data to provide more
331
+ # insights.
332
+ #
333
+ # Verbose logging is _disabled_ by default.
334
+ #
335
+ # +verbose+:: +true+ to enable verbose logging, +false+ to disable it.
336
+ # The default is +false+.
337
+ #
338
+ def self.set_verbose_logging( verbose )
339
+ @@verbose_logging = verbose
340
+ end
341
+
342
+ # Returns +true+ if verbose logging is enabled, else +false+. For more,
343
+ # see ::set_verbose_logging.
344
+ #
345
+ def self.verbose_logging?
346
+ defined?( @@verbose_logging ) ? @@verbose_logging : false
347
+ end
348
+
325
349
  # A Hoodoo::Services::Session instance to use for tests or when no
326
350
  # local Memcached instance is known about (environment variable
327
351
  # +MEMCACHED_HOST+ is not set). The session is (eventually) read each
@@ -1037,6 +1061,55 @@ module Hoodoo; module Services
1037
1061
  end
1038
1062
  end
1039
1063
 
1064
+ # Build common log information for the 'data' part of a payload based
1065
+ # on the given interaction. Returned as a Hash with Symbol keys.
1066
+ #
1067
+ # +interaction+:: Hoodoo::Services::Interaction describing a request.
1068
+ # A +context+ and +interaction_id+ are expected. The
1069
+ # +target_interface+ and +requested_action+ are optional
1070
+ # and if present result in a <tt>:target</tt> entry in
1071
+ # the returned Hash. The +context.session+ value if
1072
+ # present will be included in a <tt>:session</tt> entry;
1073
+ # if verbose logging is enabled this will be quoted in
1074
+ # full, else only identity-related parts are recorded.
1075
+ #
1076
+ def build_common_log_data_for( interaction )
1077
+
1078
+ session = interaction.context.session
1079
+ interface = interaction.target_interface
1080
+ action = interaction.requested_action
1081
+
1082
+ data = {
1083
+ :id => Hoodoo::UUID.generate(),
1084
+ :interaction_id => interaction.interaction_id
1085
+ }
1086
+
1087
+ unless session.nil?
1088
+ if self.class.verbose_logging?
1089
+ data[ :session ] = session.to_h
1090
+ else
1091
+ data[ :session ] =
1092
+ {
1093
+ 'session_id' => session.session_id,
1094
+ 'caller_id' => session.caller_id,
1095
+ 'caller_version' => session.caller_version,
1096
+ 'caller_identity_name' => session.caller_identity_name,
1097
+ 'identity' => Hoodoo::Utilities.stringify( ( session.identity || {} ).to_h() )
1098
+ }
1099
+ end
1100
+ end
1101
+
1102
+ unless interface.nil? || action.nil?
1103
+ data[ :target ] = {
1104
+ :resource => ( interface.resource || '' ).to_s,
1105
+ :version => interface.version,
1106
+ :action => ( action || '' ).to_s,
1107
+ }
1108
+ end
1109
+
1110
+ return data
1111
+ end
1112
+
1040
1113
  # Make an "inbound" call log based on the given interaction.
1041
1114
  #
1042
1115
  # +interaction+:: Hoodoo::Services::Interaction describing the inbound
@@ -1049,6 +1122,8 @@ module Hoodoo; module Services
1049
1122
  #
1050
1123
  def log_inbound_request( interaction )
1051
1124
 
1125
+ data = build_common_log_data_for( interaction )
1126
+
1052
1127
  # Annoying dance required to extract all HTTP header data from Rack.
1053
1128
 
1054
1129
  env = interaction.rack_request.env
@@ -1059,27 +1134,26 @@ module Hoodoo; module Services
1059
1134
  headers[ 'CONTENT_TYPE' ] = env[ 'CONTENT_TYPE' ]
1060
1135
  headers[ 'CONTENT_LENGTH' ] = env[ 'CONTENT_LENGTH' ]
1061
1136
 
1062
- data = {
1063
- :interaction_id => interaction.interaction_id,
1064
- :payload => {
1065
- :method => env[ 'REQUEST_METHOD', ],
1066
- :scheme => env[ 'rack.url_scheme' ],
1067
- :host => env[ 'SERVER_NAME' ],
1068
- :port => env[ 'SERVER_PORT' ],
1069
- :script => env[ 'SCRIPT_NAME' ],
1070
- :path => env[ 'PATH_INFO' ],
1071
- :query => env[ 'QUERY_STRING' ],
1072
- :headers => headers
1073
- }
1137
+ data[ :payload ] = {
1138
+ :method => env[ 'REQUEST_METHOD', ],
1139
+ :scheme => env[ 'rack.url_scheme' ],
1140
+ :host => env[ 'SERVER_NAME' ],
1141
+ :port => env[ 'SERVER_PORT' ],
1142
+ :script => env[ 'SCRIPT_NAME' ],
1143
+ :path => env[ 'PATH_INFO' ],
1144
+ :query => env[ 'QUERY_STRING' ],
1145
+ :headers => headers
1074
1146
  }
1075
1147
 
1076
1148
  # Deal with body data and security issues.
1077
1149
 
1078
- secure = true
1150
+ secure = true
1151
+ interface = interaction.target_interface
1152
+ action = interaction.requested_action
1079
1153
 
1080
- unless interaction.target_interface.nil? || interaction.requested_action.nil?
1081
- secure_log_actions = interaction.target_interface.secure_log_for()
1082
- secure_type = secure_log_actions[ interaction.requested_action ]
1154
+ unless interface.nil? || action.nil?
1155
+ secure_log_actions = interface.secure_log_for()
1156
+ secure_type = secure_log_actions[ action ]
1083
1157
 
1084
1158
  # Allow body logging if there's no security specified for this action
1085
1159
  # or the security is specified for the response only (since we log the
@@ -1089,14 +1163,6 @@ module Hoodoo; module Services
1089
1163
  # as will any other unexpected value that might get specified.
1090
1164
 
1091
1165
  secure = false if secure_type.nil? || secure_type == :response
1092
-
1093
- # Fill in unrelated useful data since we know it is available here.
1094
-
1095
- data[ :target ] = {
1096
- :resource => ( interaction.target_interface.resource || '' ).to_s,
1097
- :version => interaction.target_interface.version,
1098
- :action => ( interaction.requested_action || '' ).to_s
1099
- }
1100
1166
  end
1101
1167
 
1102
1168
  # Compile the remaining log payload and send it.
@@ -1143,17 +1209,7 @@ module Hoodoo; module Services
1143
1209
  #
1144
1210
  return if ( context.response.halt_processing? )
1145
1211
 
1146
- # Data as per Hoodoo::Logger.
1147
-
1148
- data = {
1149
- :interaction_id => interaction.interaction_id,
1150
- :session => ( interaction.context.session || {} ).to_h,
1151
- :target => {
1152
- :resource => ( interface.resource || '' ).to_s,
1153
- :version => interface.version,
1154
- :action => ( action || '' ).to_s,
1155
- }
1156
- }
1212
+ data = build_common_log_data_for( interaction )
1157
1213
 
1158
1214
  # Don't bother logging list responses - they could be huge - instead
1159
1215
  # log all list-related parameters from the inbound request. At least
@@ -1174,7 +1230,7 @@ module Hoodoo; module Services
1174
1230
 
1175
1231
  unless interface.nil? || action.nil?
1176
1232
  secure_log_actions = interface.secure_log_for()
1177
- secure_type = secure_log_actions[ action ]
1233
+ secure_type = secure_log_actions[ action ]
1178
1234
 
1179
1235
  # Allow body logging if there's no security specified for this action
1180
1236
  # or the security is specified for the request only (since we log the
@@ -1211,25 +1267,22 @@ module Hoodoo; module Services
1211
1267
  # the interaction currently being logged.
1212
1268
  #
1213
1269
  def log_outbound_response( interaction, rack_data )
1214
- secure = true
1215
- id = nil
1216
- level = if interaction.context.response.halt_processing?
1217
- :error
1218
- else
1219
- :info
1220
- end
1221
1270
 
1222
- data = {
1223
- :interaction_id => interaction.interaction_id,
1224
- :payload => {
1225
- :http_status_code => rack_data[ 0 ].to_i,
1226
- :http_headers => rack_data[ 1 ]
1227
- }
1271
+ level = interaction.context.response.halt_processing? ? :error : :info
1272
+ data = build_common_log_data_for( interaction )
1273
+
1274
+ data[ :payload ] = {
1275
+ :http_status_code => rack_data[ 0 ].to_i,
1276
+ :http_headers => rack_data[ 1 ]
1228
1277
  }
1229
1278
 
1230
- unless interaction.target_interface.nil? || interaction.requested_action.nil?
1231
- secure_log_actions = interaction.target_interface.secure_log_for()
1232
- secure_type = secure_log_actions[ interaction.requested_action ]
1279
+ secure = true
1280
+ interface = interaction.target_interface
1281
+ action = interaction.requested_action
1282
+
1283
+ unless interface.nil? || action.nil?
1284
+ secure_log_actions = interface.secure_log_for()
1285
+ secure_type = secure_log_actions[ action ]
1233
1286
 
1234
1287
  # Allow body logging if there's no security specified for this action
1235
1288
  # or the security is specified for the request only (since we log the
@@ -1239,12 +1292,6 @@ module Hoodoo; module Services
1239
1292
  # as will any other unexpected value that might get specified.
1240
1293
 
1241
1294
  secure = false if secure_type.nil? || secure_type == :request
1242
-
1243
- data[ :target ] = {
1244
- :resource => ( interaction.target_interface.resource || '' ).to_s,
1245
- :version => interaction.target_interface.version,
1246
- :action => ( interaction.requested_action || '' ).to_s
1247
- }
1248
1295
  end
1249
1296
 
1250
1297
  if secure == false || level == :error
@@ -1263,7 +1310,9 @@ module Hoodoo; module Services
1263
1310
  # persistence layers store the item as an error with the correct ID.
1264
1311
 
1265
1312
  body = ::JSON.parse( body )
1266
- id = body[ 'id' ]
1313
+
1314
+ uuid = body[ 'id' ]
1315
+ data[ :id ] = uuid unless uuid.nil?
1267
1316
  rescue
1268
1317
  end
1269
1318
 
@@ -1275,9 +1324,6 @@ module Hoodoo; module Services
1275
1324
  data[ :payload ][ :response_body ] = body
1276
1325
  end
1277
1326
 
1278
- data[ :id ] = id unless id.nil?
1279
- data[ :session ] = interaction.context.session.to_h unless interaction.context.session.nil?
1280
-
1281
1327
  @@logger.report(
1282
1328
  level,
1283
1329
  :Middleware,
@@ -1310,17 +1356,14 @@ module Hoodoo; module Services
1310
1356
 
1311
1357
  return unless @@logger.report?( :debug )
1312
1358
 
1359
+ data = build_common_log_data_for( interaction )
1360
+
1313
1361
  scheme = interaction.rack_request.scheme || 'unknown_scheme'
1314
1362
  host_with_port = interaction.rack_request.host_with_port || 'unknown_host'
1315
1363
  full_path = interaction.rack_request.fullpath || '/unknown_path'
1316
1364
 
1317
- data = {
1318
- :full_uri => "#{ scheme }://#{ host_with_port }#{ full_path }",
1319
- :interaction_id => interaction.interaction_id,
1320
- :payload => { 'args' => args }
1321
- }
1322
-
1323
- data[ :session ] = interaction.context.session.to_h unless interaction.context.session.nil?
1365
+ data[ :full_uri ] = "#{ scheme }://#{ host_with_port }#{ full_path }"
1366
+ data[ :payload ] = { 'args' => args }
1324
1367
 
1325
1368
  @@logger.report(
1326
1369
  :debug,
@@ -1353,6 +1396,35 @@ module Hoodoo; module Services
1353
1396
  def respond_for( interaction, preflight = false )
1354
1397
  interaction.context.response.body = '' if preflight
1355
1398
 
1399
+ # Oddly placed code for efficiency and sanity.
1400
+ #
1401
+ # When #log_outbound_response is called below, it would make sense to
1402
+ # use its existing code path for logging errors and include a variant of
1403
+ # the "if" below to add the X-Error-Logged-Via-Alchemy HTTP header if
1404
+ # required down at that level.
1405
+ #
1406
+ # However, we want the Rack response payload all wrapped up for the log
1407
+ # and it's generated here, then passed in; somehow the logging method
1408
+ # would need to update the now-compiled Rack data, or we generate the
1409
+ # Rack data again on exit, but then the logged Rack data would be wrong.
1410
+ #
1411
+ # To solve all this, just deal with the an-error-was-logged header here,
1412
+ # before we log anything or generate Rack information.
1413
+
1414
+ if (
1415
+ interaction.context.response.halt_processing? &&
1416
+ self.class.on_queue? &&
1417
+ defined?( @@alchemy ) &&
1418
+ @@logger.include_class?( Hoodoo::Services::Middleware::AMQPLogWriter )
1419
+ )
1420
+
1421
+ interaction.context.response.add_header(
1422
+ 'X-Error-Logged-Via-Alchemy',
1423
+ 'yes',
1424
+ true
1425
+ )
1426
+ end
1427
+
1356
1428
  rack_data = interaction.context.response.for_rack()
1357
1429
  log_outbound_response( interaction, rack_data )
1358
1430
 
@@ -32,6 +32,14 @@ module Hoodoo
32
32
  #
33
33
  attr_accessor :caller_id
34
34
 
35
+ # An optional property of a session is the Caller's "identity name",
36
+ # a generic way to refer to this Caller which will appear in logs.
37
+ # The use is up to the session creator, in combination with whatever
38
+ # logging engine is in use; if it ascribes meaning to the identity
39
+ # name, then the session creator must ensure it comforms.
40
+ #
41
+ attr_accessor :caller_identity_name
42
+
35
43
  # Callers can change; if so, related sessions must be invalidated.
36
44
  # This must be achieved by keeping a version count on the Caller. A
37
45
  # session is associated with a particular Caller version and if the
@@ -192,7 +200,7 @@ module Hoodoo
192
200
  self.to_h(),
193
201
  TTL )
194
202
 
195
- rescue Exception => exception
203
+ rescue => exception
196
204
 
197
205
  # Log error and return nil if the session can't be parsed
198
206
  #
@@ -245,7 +253,7 @@ module Hoodoo
245
253
  end
246
254
  end
247
255
 
248
- rescue Exception => exception
256
+ rescue => exception
249
257
 
250
258
  # Log error and return nil if the session can't be parsed
251
259
  #
@@ -301,7 +309,7 @@ module Hoodoo
301
309
  return :ok
302
310
  end
303
311
 
304
- rescue Exception => exception
312
+ rescue => exception
305
313
 
306
314
  # Log error and return nil if the session can't be parsed
307
315
  #
@@ -337,7 +345,7 @@ module Hoodoo
337
345
  return :not_found
338
346
  end
339
347
 
340
- rescue Exception => exception
348
+ rescue => exception
341
349
 
342
350
  # Log error and return nil if the session can't be parsed
343
351
  #
@@ -376,6 +384,7 @@ module Hoodoo
376
384
  session_id
377
385
  caller_id
378
386
  caller_version
387
+ caller_identity_name
379
388
 
380
389
  ).each do | property |
381
390
  value = self.send( property )
@@ -420,6 +429,7 @@ module Hoodoo
420
429
  session_id
421
430
  caller_id
422
431
  caller_version
432
+ caller_identity_name
423
433
 
424
434
  ).each do | property |
425
435
  value = hash[ property ]
@@ -549,8 +559,9 @@ module Hoodoo
549
559
  raise 'Hoodoo::Services::Session.connect_to_memcached: The Memcached connection host data is nil or empty'
550
560
  end
551
561
 
552
- stats = nil
553
- mclient = nil
562
+ exception = nil
563
+ stats = nil
564
+ mclient = nil
554
565
 
555
566
  begin
556
567
  @@dalli_clients ||= {}
@@ -565,13 +576,17 @@ module Hoodoo
565
576
 
566
577
  stats = @@dalli_clients[ host ].stats()
567
578
 
568
- rescue Exception => e
569
- stats = nil
579
+ rescue => e
580
+ exception = e
570
581
 
571
582
  end
572
583
 
573
584
  if stats.nil?
574
- raise "Hoodoo::Services::Session.connect_to_memcached: Cannot connect to Memcached at '#{ host }'"
585
+ if exception.nil?
586
+ raise "Hoodoo::Services::Session.connect_to_memcached: Did not get back meaningful data from Memcached at '#{ host }'"
587
+ else
588
+ raise "Hoodoo::Services::Session.connect_to_memcached: Cannot connect to Memcached at '#{ host }': #{ exception.to_s }"
589
+ end
575
590
  else
576
591
  return @@dalli_clients[ host ]
577
592
  end
@@ -12,6 +12,6 @@ module Hoodoo
12
12
  # The Hoodoo gem version. If this changes, ensure that the date in
13
13
  # "hoodoo.gemspec" is correct and run "bundle install" (or "update").
14
14
  #
15
- VERSION = '1.5.2'
15
+ VERSION = '1.6.0'
16
16
 
17
17
  end
@@ -51,7 +51,7 @@ describe Hoodoo::Logger do
51
51
  @logger.error( 'Three instances of StreamWriter' )
52
52
  end
53
53
 
54
- it 'removes requested instances' do
54
+ it 'removes a requested instance' do
55
55
  @logger.remove( @stderr_2 )
56
56
 
57
57
  expect( @stderr_1 ).to receive( :report ).once
@@ -62,6 +62,17 @@ describe Hoodoo::Logger do
62
62
  @logger.error( 'Two instances of StreamWriter' )
63
63
  end
64
64
 
65
+ it 'removes two requested instances' do
66
+ @logger.remove( @stderr_3, @stderr_2 )
67
+
68
+ expect( @stderr_1 ).to receive( :report ).once
69
+ expect( @stderr_2 ).to_not receive( :report )
70
+ expect( @stderr_3 ).to_not receive( :report )
71
+ expect( $stderr ).to_not receive( :puts )
72
+
73
+ @logger.error( 'One instance of StreamWriter' )
74
+ end
75
+
65
76
  it 'removes all instances' do
66
77
  @logger.remove_all
67
78
 
@@ -161,6 +172,61 @@ describe Hoodoo::Logger do
161
172
 
162
173
  # ===========================================================================
163
174
 
175
+ context 'enquiries' do
176
+ before :each do
177
+ @logger = described_class.new
178
+ end
179
+
180
+ it 'via include(s)? work' do
181
+ a = Hoodoo::Logger::StreamWriter.new( $stderr )
182
+ b = Hoodoo::Logger::StreamWriter.new( $stderr )
183
+ c = Hoodoo::Logger::StreamWriter.new( $stderr )
184
+
185
+ @logger.add( a, b )
186
+
187
+ expect( @logger.include?( a ) ).to eq( true )
188
+ expect( @logger.include?( b ) ).to eq( true )
189
+ expect( @logger.include?( c ) ).to eq( false )
190
+
191
+ expect( @logger.includes?( a ) ).to eq( true )
192
+ expect( @logger.includes?( b ) ).to eq( true )
193
+ expect( @logger.includes?( c ) ).to eq( false )
194
+ end
195
+
196
+ it 'via include(s)_class? work' do
197
+ a = Hoodoo::Logger::StreamWriter.new( $stderr )
198
+ b = Hoodoo::Logger::StreamWriter.new( $stderr )
199
+ c = Hoodoo::Logger::FileWriter.new( 'file1' )
200
+ d = Hoodoo::Logger::FileWriter.new( 'file2' )
201
+
202
+ @logger.add( a, b )
203
+
204
+ expect( @logger.include_class?( Hoodoo::Logger::StreamWriter ) ).to eq( true )
205
+ expect( @logger.include_class?( Hoodoo::Logger::FileWriter ) ).to eq( false )
206
+
207
+ expect( @logger.includes_class?( Hoodoo::Logger::StreamWriter ) ).to eq( true )
208
+ expect( @logger.includes_class?( Hoodoo::Logger::FileWriter ) ).to eq( false )
209
+
210
+ @logger.add( c, d )
211
+
212
+ expect( @logger.include_class?( Hoodoo::Logger::StreamWriter ) ).to eq( true )
213
+ expect( @logger.include_class?( Hoodoo::Logger::FileWriter ) ).to eq( true )
214
+
215
+ expect( @logger.includes_class?( Hoodoo::Logger::StreamWriter ) ).to eq( true )
216
+ expect( @logger.includes_class?( Hoodoo::Logger::FileWriter ) ).to eq( true )
217
+
218
+ @logger.remove( a, b )
219
+
220
+ expect( @logger.include_class?( Hoodoo::Logger::StreamWriter ) ).to eq( false )
221
+ expect( @logger.include_class?( Hoodoo::Logger::FileWriter ) ).to eq( true )
222
+
223
+ expect( @logger.includes_class?( Hoodoo::Logger::StreamWriter ) ).to eq( false )
224
+ expect( @logger.includes_class?( Hoodoo::Logger::FileWriter ) ).to eq( true )
225
+ end
226
+ end
227
+
228
+ # ===========================================================================
229
+
164
230
  context 'legacy logging' do
165
231
  before :each do
166
232
  @logger = described_class.new
@@ -35,6 +35,7 @@ describe Hoodoo::Services::Middleware::AMQPLogWriter do
35
35
  @identity_id_1 = Hoodoo::UUID.generate
36
36
  @identity_id_2 = Hoodoo::UUID.generate
37
37
  @identity_id_3 = Hoodoo::UUID.generate
38
+ @identity_id_4 = Hoodoo::UUID.generate
38
39
 
39
40
  @authorised_ids = [ Hoodoo::UUID.generate, Hoodoo::UUID.generate ]
40
41
  @authorised_codes = [ 'CODE_A', 'CODE_B' ]
@@ -50,6 +51,8 @@ describe Hoodoo::Services::Middleware::AMQPLogWriter do
50
51
  :authorised_codes => @authorised_codes
51
52
  }
52
53
 
54
+ @session.caller_identity_name = @identity_id_4
55
+
53
56
  @alchemy = OpenStruct.new
54
57
  @queue = 'foo.bar'
55
58
  @logger = described_class.new( @alchemy, @queue )
@@ -65,22 +68,28 @@ describe Hoodoo::Services::Middleware::AMQPLogWriter do
65
68
  interaction_id = Hoodoo::UUID.generate
66
69
  data = {
67
70
  :id => id,
68
- :session => @session.to_h(),
69
- :interaction_id => interaction_id
71
+ :interaction_id => interaction_id,
72
+ :session =>
73
+ {
74
+ 'caller_id' => @session.caller_id,
75
+ 'caller_identity_name' => @session.caller_identity_name,
76
+ 'identity' => @session.identity.to_h()
77
+ }
70
78
  }
71
79
 
72
80
  expected_hash = {
73
- :id => id,
74
- :level => 'warn',
75
- :component => component,
76
- :code => code,
77
- :reported_at => reported_at,
81
+ :id => id,
82
+ :level => 'warn',
83
+ :component => component,
84
+ :code => code,
85
+ :reported_at => reported_at,
78
86
 
79
- :data => data,
87
+ :interaction_id => interaction_id,
88
+ :data => data,
80
89
 
81
- :interaction_id => interaction_id,
82
- :caller_id => @session.caller_id,
83
- :identity => Hoodoo::Utilities.stringify( @session.identity.to_h )
90
+ :caller_id => @caller_id,
91
+ :caller_identity_name => @identity_id_4,
92
+ :identity => Hoodoo::Utilities.stringify( @session.identity.to_h )
84
93
  }
85
94
 
86
95
  expect( @alchemy ).to receive( :send_message_to_service ).with( @queue, { "body" => expected_hash.to_json } ).once
@@ -123,13 +123,6 @@ describe Hoodoo::Services::Middleware do
123
123
  end
124
124
  end
125
125
 
126
- def app
127
- Rack::Builder.new do
128
- use Hoodoo::Services::Middleware
129
- run TestLogService.new
130
- end
131
- end
132
-
133
126
  it 'has the expected "test" mode loggers' do
134
127
  instances = force_logging_to( 'test' )
135
128
 
@@ -235,9 +228,91 @@ describe Hoodoo::Services::Middleware do
235
228
  expect( instances[ 0 ] ).to be_a( Hoodoo::Services::Middleware::AMQPLogWriter )
236
229
  expect( Hoodoo::Services::Middleware.logger.level ).to eq( :info )
237
230
  end
231
+
232
+ # When logging to Alchemy, the middleware should set an
233
+ # "X-Error-Logged-Via-Alchemy" HTTP header. An Alchemy router/edge
234
+ # splitter component can use this to avoid double-logging error data
235
+ # if it implements a middleware-based approach to error reporting.
236
+ #
237
+ context '"X-Error-Logged-Via-Alchemy" HTTP header is set for' do
238
+ before :each do
239
+ force_logging_to( 'development' )
240
+ expect_any_instance_of(FakeAlchemy).to receive(:send_message_to_service).at_least(:once)
241
+ end
242
+
243
+ it 'service errors' do
244
+ expect_any_instance_of( TestLogImplementation ).to receive( :show ) do | instance, context |
245
+ context.response.not_found( context.request.ident )
246
+ end
247
+
248
+ spec_helper_silence_stdout do
249
+ get '/v1/test_log/hello', nil, { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
250
+ end
251
+
252
+ expect( last_response.headers[ 'X-Error-Logged-Via-Alchemy' ] ).to eq( 'yes' )
253
+ expect( last_response.status ).to eq( 404 )
254
+ end
255
+
256
+ it 'service exceptions' do
257
+ expect_any_instance_of( TestLogImplementation ).to receive( :show ) do
258
+ raise "boo!"
259
+ end
260
+
261
+ spec_helper_silence_stdout do
262
+ get '/v1/test_log/hello', nil, { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
263
+ end
264
+
265
+ expect( last_response.headers[ 'X-Error-Logged-Via-Alchemy' ] ).to eq( 'yes' )
266
+ expect( last_response.status ).to eq( 500 )
267
+ end
268
+
269
+ context 'invalid' do
270
+ before :each do
271
+ @old_test_session = Hoodoo::Services::Middleware.test_session()
272
+ end
273
+
274
+ after :each do
275
+ Hoodoo::Services::Middleware.set_test_session( @old_test_session )
276
+ end
277
+
278
+ it 'session errors' do
279
+ Hoodoo::Services::Middleware.set_test_session( nil )
280
+
281
+ spec_helper_silence_stdout do
282
+ get '/v1/test_log/hello', nil, { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
283
+ end
284
+
285
+ expect( last_response.headers[ 'X-Error-Logged-Via-Alchemy' ] ).to eq( 'yes' )
286
+ expect( last_response.status ).to eq( 401 )
287
+ end
288
+
289
+ it 'permission errors' do
290
+ session = @old_test_session.dup()
291
+ session.permissions = Hoodoo::Services::Permissions.new # Default is 'deny all'
292
+
293
+ Hoodoo::Services::Middleware.set_test_session( session )
294
+
295
+ spec_helper_silence_stdout do
296
+ get '/v1/test_log/hello', nil, { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
297
+ end
298
+
299
+ expect( last_response.headers[ 'X-Error-Logged-Via-Alchemy' ] ).to eq( 'yes' )
300
+ expect( last_response.status ).to eq( 403 )
301
+ end
302
+ end
303
+
304
+ # Middleware exceptions are not tested. There's no way to know the kind
305
+ # of failure that might happen in the real-world and the point at which
306
+ # the exception occurs in the processing chain may influence whether or
307
+ # not the header gets set. We might end up with double logging from an
308
+ # router/edge splitter in such cases, but that's acceptable for what
309
+ # should be an extremely rare event.
310
+
311
+ end
238
312
  end
239
313
 
240
- context 'secure logging' do
314
+ context 'with' do
315
+
241
316
  class HashLogger < Hoodoo::Logger::FastWriter
242
317
  @@log_data = []
243
318
 
@@ -301,56 +376,109 @@ describe Hoodoo::Services::Middleware do
301
376
  end
302
377
  end
303
378
 
304
- # To test_log, 'create' says secure for 'request'
305
- #
306
- it 'does not log creation requests unexpectedly' do
307
- post '/v1/test_log', '{ "foo": "bar" }', { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
379
+ context 'verbose logging' do
380
+ context 'which is turned off' do
381
+ it 'by default' do
382
+ expect( Hoodoo::Services::Middleware.verbose_logging? ).to eq( false )
383
+ end
308
384
 
309
- inbound, result, outbound = get_data_for( :create )
310
- check_common_entries( 'TestLog', 1, 'create', inbound.last, result.first, outbound.first )
385
+ it 'and omits non-identity information in the session section' do
386
+ get '/v1/test_log/hello', nil, { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
311
387
 
312
- entry = inbound.last; expect( entry[ :data ][ :payload ] ).to_not have_key( :body )
313
- entry = result.first; expect( entry[ :data ] ).to have_key( :payload )
314
- entry = outbound.first; expect( entry[ :data ][ :payload ] ).to have_key( :response_body )
315
- end
388
+ inbound, result, outbound = get_data_for( :show )
389
+ data = result.first[ :data ]
316
390
 
317
- # To test_log, 'update' says secure for 'request'
318
- #
319
- it 'does not log update responses unexpectedly' do
320
- patch '/v1/test_log/foo', '{ "foo": "bar" }', { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
391
+ expect( data[ :session ][ 'session_id' ] ).to be_present
392
+ expect( data[ :session ][ 'caller_id' ] ).to be_present
393
+ expect( data[ :session ][ 'caller_version' ] ).to be_present
394
+ expect( data[ :session ][ 'caller_identity_name' ] ).to be_present
395
+ expect( data[ :session ][ 'identity' ] ).to be_present
321
396
 
322
- inbound, result, outbound = get_data_for( :update )
323
- check_common_entries( 'TestLog', 1, 'update', inbound.last, result.first, outbound.first )
397
+ expect( data[ :session ][ 'permissions' ] ).to_not be_present
398
+ expect( data[ :session ][ 'scoping' ] ).to_not be_present
399
+ end
400
+ end
324
401
 
325
- entry = inbound.last; expect( entry[ :data ][ :payload ] ).to have_key( :body )
326
- entry = result.first; expect( entry[ :data ] ).to_not have_key( :payload )
327
- entry = outbound.first; expect( entry[ :data ][ :payload ] ).to_not have_key( :response_body )
328
- end
402
+ context 'which is turned on' do
403
+ before :each do
404
+ @old_verbose = Hoodoo::Services::Middleware.verbose_logging?
405
+ Hoodoo::Services::Middleware.set_verbose_logging( true )
406
+ end
329
407
 
330
- # To test_log_b, 'create' says secure for 'both'.
331
- #
332
- it 'does not log requests or responses unexpectedly' do
333
- post '/v2/test_log_b', '{ "foo": "bar" }', { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
408
+ after :each do
409
+ Hoodoo::Services::Middleware.set_verbose_logging( @old_verbose )
410
+ end
411
+
412
+ it 'and includes non-identity information in the session section' do
413
+ get '/v1/test_log/hello', nil, { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
334
414
 
335
- inbound, result, outbound = get_data_for( :create )
336
- check_common_entries( 'TestLogB', 2, 'create', inbound.last, result.first, outbound.first )
415
+ inbound, result, outbound = get_data_for( :show )
416
+ data = result.first[ :data ]
337
417
 
338
- entry = inbound.last; expect( entry[ :data ][ :payload ] ).to_not have_key( :body )
339
- entry = result.first; expect( entry[ :data ] ).to_not have_key( :payload )
340
- entry = outbound.first; expect( entry[ :data ][ :payload ] ).to_not have_key( :response_body )
418
+ expect( data[ :session ][ 'session_id' ] ).to be_present
419
+ expect( data[ :session ][ 'caller_id' ] ).to be_present
420
+ expect( data[ :session ][ 'caller_version' ] ).to be_present
421
+ expect( data[ :session ][ 'caller_identity_name' ] ).to be_present
422
+ expect( data[ :session ][ 'identity' ] ).to be_present
423
+ expect( data[ :session ][ 'permissions' ] ).to be_present
424
+ expect( data[ :session ][ 'scoping' ] ).to be_present
425
+ end
426
+ end
341
427
  end
342
428
 
343
- # To test_log_b, 'update' does not ask for security.
344
- #
345
- it 'does not log requests or responses unexpectedly' do
346
- patch '/v2/test_log_b/foo', '{ "foo": "bar" }', { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
429
+ context 'secure logging' do
347
430
 
348
- inbound, result, outbound = get_data_for( :update )
349
- check_common_entries( 'TestLogB', 2, 'update', inbound.last, result.first, outbound.first )
431
+ # To test_log, 'create' says secure for 'request'
432
+ #
433
+ it 'does not log creation requests unexpectedly' do
434
+ post '/v1/test_log', '{ "foo": "bar" }', { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
350
435
 
351
- entry = inbound.last; expect( entry[ :data ][ :payload ] ).to have_key( :body )
352
- entry = result.first; expect( entry[ :data ] ).to have_key( :payload )
353
- entry = outbound.first; expect( entry[ :data ][ :payload ] ).to have_key( :response_body )
436
+ inbound, result, outbound = get_data_for( :create )
437
+ check_common_entries( 'TestLog', 1, 'create', inbound.last, result.first, outbound.first )
438
+
439
+ entry = inbound.last; expect( entry[ :data ][ :payload ] ).to_not have_key( :body )
440
+ entry = result.first; expect( entry[ :data ] ).to have_key( :payload )
441
+ entry = outbound.first; expect( entry[ :data ][ :payload ] ).to have_key( :response_body )
442
+ end
443
+
444
+ # To test_log, 'update' says secure for 'request'
445
+ #
446
+ it 'does not log update responses unexpectedly' do
447
+ patch '/v1/test_log/foo', '{ "foo": "bar" }', { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
448
+
449
+ inbound, result, outbound = get_data_for( :update )
450
+ check_common_entries( 'TestLog', 1, 'update', inbound.last, result.first, outbound.first )
451
+
452
+ entry = inbound.last; expect( entry[ :data ][ :payload ] ).to have_key( :body )
453
+ entry = result.first; expect( entry[ :data ] ).to_not have_key( :payload )
454
+ entry = outbound.first; expect( entry[ :data ][ :payload ] ).to_not have_key( :response_body )
455
+ end
456
+
457
+ # To test_log_b, 'create' says secure for 'both'.
458
+ #
459
+ it 'does not log requests or responses unexpectedly' do
460
+ post '/v2/test_log_b', '{ "foo": "bar" }', { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
461
+
462
+ inbound, result, outbound = get_data_for( :create )
463
+ check_common_entries( 'TestLogB', 2, 'create', inbound.last, result.first, outbound.first )
464
+
465
+ entry = inbound.last; expect( entry[ :data ][ :payload ] ).to_not have_key( :body )
466
+ entry = result.first; expect( entry[ :data ] ).to_not have_key( :payload )
467
+ entry = outbound.first; expect( entry[ :data ][ :payload ] ).to_not have_key( :response_body )
468
+ end
469
+
470
+ # To test_log_b, 'update' does not ask for security.
471
+ #
472
+ it 'does not log requests or responses unexpectedly' do
473
+ patch '/v2/test_log_b/foo', '{ "foo": "bar" }', { 'CONTENT_TYPE' => 'application/json; charset=utf-8' }
474
+
475
+ inbound, result, outbound = get_data_for( :update )
476
+ check_common_entries( 'TestLogB', 2, 'update', inbound.last, result.first, outbound.first )
477
+
478
+ entry = inbound.last; expect( entry[ :data ][ :payload ] ).to have_key( :body )
479
+ entry = result.first; expect( entry[ :data ] ).to have_key( :payload )
480
+ entry = outbound.first; expect( entry[ :data ][ :payload ] ).to have_key( :response_body )
481
+ end
354
482
  end
355
483
  end
356
484
  end
@@ -293,7 +293,13 @@ describe Hoodoo::Services::Session do
293
293
  raise 'Mock Memcached connection failure'
294
294
  end
295
295
 
296
- expect( Hoodoo::Services::Middleware.logger ).to receive( :warn ).once.and_call_original
296
+ expect( Hoodoo::Services::Middleware.logger ).to(
297
+ receive( :warn ).once.with(
298
+ 'Hoodoo::Services::Session\\#load_from_memcached!: Session loading failed - connection fault or session corrupt',
299
+ 'Mock Memcached connection failure'
300
+ ).and_call_original
301
+ )
302
+
297
303
  expect( loader.load_from_memcached!( '1234' ) ).to eq( :fail )
298
304
  end
299
305
 
@@ -303,11 +309,20 @@ describe Hoodoo::Services::Session do
303
309
  :caller_version => 1
304
310
  )
305
311
 
312
+ # The first 'set' call is an attempt to update the caller version before
313
+ # the updated session is saved.
314
+
306
315
  expect_any_instance_of( Hoodoo::Services::Session::MockDalliClient ).to receive( :set ).once do
307
316
  raise 'Mock Memcached connection failure'
308
317
  end
309
318
 
310
- expect( Hoodoo::Services::Middleware.logger ).to receive( :warn ).once.and_call_original
319
+ expect( Hoodoo::Services::Middleware.logger ).to(
320
+ receive( :warn ).once.with(
321
+ 'Hoodoo::Services::Session\\#update_caller_version_in_memcached: Client version update - connection fault or corrupt record',
322
+ 'Mock Memcached connection failure'
323
+ ).and_call_original
324
+ )
325
+
311
326
  expect( s.save_to_memcached() ).to eq( :fail )
312
327
  end
313
328
 
@@ -322,7 +337,13 @@ describe Hoodoo::Services::Session do
322
337
  raise 'Mock Memcached connection failure'
323
338
  end
324
339
 
325
- expect( Hoodoo::Services::Middleware.logger ).to receive( :warn ).once.and_call_original
340
+ expect( Hoodoo::Services::Middleware.logger ).to(
341
+ receive( :warn ).once.with(
342
+ 'Hoodoo::Services::Session\\#save_to_memcached: Session saving failed - connection fault or session corrupt',
343
+ 'Mock Memcached connection failure'
344
+ ).and_call_original
345
+ )
346
+
326
347
  expect( s.save_to_memcached() ).to eq( :fail )
327
348
  end
328
349
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hoodoo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.2
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Loyalty New Zealand
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-18 00:00:00.000000000 Z
11
+ date: 2016-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kgio