hoodoo 1.5.2 → 1.6.0

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