service_skeleton 0.0.0.30.g32b8169 → 0.0.0.48.g4a40599

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +260 -143
  3. data/lib/service_skeleton.rb +22 -186
  4. data/lib/service_skeleton/config.rb +58 -31
  5. data/lib/service_skeleton/config_class.rb +16 -0
  6. data/lib/service_skeleton/config_variable.rb +24 -16
  7. data/lib/service_skeleton/config_variable/boolean.rb +21 -0
  8. data/lib/service_skeleton/config_variable/enum.rb +27 -0
  9. data/lib/service_skeleton/config_variable/float.rb +25 -0
  10. data/lib/service_skeleton/config_variable/integer.rb +25 -0
  11. data/lib/service_skeleton/config_variable/kv_list.rb +26 -0
  12. data/lib/service_skeleton/config_variable/path_list.rb +13 -0
  13. data/lib/service_skeleton/config_variable/string.rb +18 -0
  14. data/lib/service_skeleton/config_variable/url.rb +36 -0
  15. data/lib/service_skeleton/config_variable/yaml_file.rb +42 -0
  16. data/lib/service_skeleton/config_variables.rb +49 -82
  17. data/lib/service_skeleton/error.rb +5 -3
  18. data/lib/service_skeleton/filtering_logger.rb +2 -0
  19. data/lib/service_skeleton/generator.rb +165 -0
  20. data/lib/service_skeleton/logging_helpers.rb +5 -3
  21. data/lib/service_skeleton/metric_method_name.rb +9 -0
  22. data/lib/service_skeleton/metrics_methods.rb +28 -13
  23. data/lib/service_skeleton/runner.rb +46 -0
  24. data/lib/service_skeleton/service_name.rb +20 -0
  25. data/lib/service_skeleton/signal_manager.rb +202 -0
  26. data/lib/service_skeleton/signals_methods.rb +15 -0
  27. data/lib/service_skeleton/ultravisor_children.rb +17 -0
  28. data/lib/service_skeleton/ultravisor_loggerstash.rb +11 -0
  29. data/service_skeleton.gemspec +8 -7
  30. metadata +65 -15
  31. data/lib/service_skeleton/background_worker.rb +0 -89
  32. data/lib/service_skeleton/signal_handler.rb +0 -195
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c73ba4e02f8bcd6f90caa188e15b3da3bd3fef747a30f8da70ac12881177c179
4
- data.tar.gz: a1e1f008182f70f135c7455725d2e1bf4a7515789512786b2aa58c914bed9299
3
+ metadata.gz: 79f6fcf6e15dbc20ca839d72de774a57a45322af64bdf218198e130239ea03f0
4
+ data.tar.gz: 3ffcdfb56114cf8cf3ba52eed3e9ceffdd8798e3c35f98d34d9e6ed73f4a8f9c
5
5
  SHA512:
6
- metadata.gz: ee7d4bb7bb7f1b84f37fb3d63bc698e632dcad00eaa98e886628106d3966d57ce950499696ea04628fb09f8e1de9b6242069486fc0669be2413278dcf6c8f744
7
- data.tar.gz: 5d75bab29a82cd40083fde69a2d9725273475a45e922d68e2b3b88300215fc3ec0dfea08fa441556235fe48a0879fe5cbff037cf407debd9a3efd5f2710c18aa
6
+ metadata.gz: da29c0aa008026bc92249ed58105037a98e71291335cfa21292847fdeda0dd4c96644ba30ec613df630a0014abf15997badd61c064c5db1e09583dcad4ad9c87
7
+ data.tar.gz: 94afc66af6e14f50e0485335dc7f245947df5df9aed970e50fd94da9d46be803bc5479904f578ba6e8cc3e92bf4dee5e95153956605e9a2618f7a97eb2d0f4a7
data/README.md CHANGED
@@ -6,10 +6,11 @@ other parts of a larger system. It provides:
6
6
  * Prometheus-based metrics registry;
7
7
  * Signal handling;
8
8
  * Configuration extraction from the process environment;
9
+ * Supervision and automated restarting of your service code;
9
10
  * and more.
10
11
 
11
12
  The general philosophy of `ServiceSkeleton` is to provide features which have
12
- been found to be almost universally necessary, in modern deployment
13
+ been found to be almost universally necessary in modern deployment
13
14
  configurations, to prefer convenience over configuration, and to always be
14
15
  secure by default.
15
16
 
@@ -40,7 +41,9 @@ like this:
40
41
 
41
42
  require "service_skeleton"
42
43
 
43
- class HelloService < ServiceSkeleton
44
+ class HelloService
45
+ include ServiceSkeleton
46
+
44
47
  def run
45
48
  loop do
46
49
  puts "Hello, Service!"
@@ -49,69 +52,132 @@ like this:
49
52
  end
50
53
  end
51
54
 
52
- HelloService.new(ENV).start if __FILE__ == $0
55
+ ServiceSkeleton::Runner.new(HelloService, ENV).run if __FILE__ == $0
53
56
 
54
57
  First, we require the `"service_skeleton"` library, which is a pre-requisite
55
- for the `ServiceSkeleton` base class to be available, which is subclassed by
56
- `HelloService`. The `run` method is where you put your service's work,
57
- typically in an infinite loop, because services are long-running, persistent
58
- processes.
58
+ for the `ServiceSkeleton` module to be available. Your code is placed in
59
+ its own class in the `run` method, where you put your service's logic. The
60
+ `ServiceSkeleton` module provides helper methods and initializers, which will
61
+ be introduced as we go along.
62
+
63
+ The `run` method is typically an infinite loop, because services are long-running,
64
+ persistent processes. If you `run` method exits, or raises an unhandled exception,
65
+ the supervisor will restart it.
59
66
 
60
- Finally, the last line instantiates an instance of our service (passing the
61
- process environment in, for configuration), and then calls `.run` on that
62
- object -- but only if we were called as a standalone program (rather than
63
- being `require`'d ourselves).
67
+ Finally, the last line uses the `ServiceSkeleton::Runner` class to actually run
68
+ your service. This ensures that all of the scaffolding services, like the
69
+ signal handler and metrics server, are up and running alongside your service
70
+ code.
64
71
 
65
72
 
66
73
  ## The `#run` loop
67
74
 
68
75
  The core of a service is usually some sort of infinite loop, which waits for a
69
- reason to do something, and then does it. A lot of services are
70
- network-accessible, and so the "reason to do something" is "because someone
71
- made a connection to a port I'm listening". Other times it could be because of
72
- a periodic timer, a filesystem event, or anything else that takes your fancy.
76
+ reason to do something, and then does it. A lot of services are network
77
+ accessible, and so the "reason to do something" is "because someone made a
78
+ connection to a port on which I'm listening". Other times it could be because
79
+ of a periodic timer firing, a filesystem event, or anything else that takes
80
+ your fancy.
73
81
 
74
82
  Whatever it is, `ServiceSkeleton` doesn't discriminate. All you have to do is
75
- write it in your subclass' `#run` method, and `ServiceSkeleton` will take care
76
- of the rest.
83
+ write it in your service class' `#run` method, and we'll take care of the rest.
77
84
 
78
85
 
79
86
  ### STAHP!
80
87
 
81
- Because the range of what a service can do is so broad, we can't provide a
82
- generic way to stop your service. You'll want to provide a (thread-safe)
83
- `#shutdown` method, which will gracefully cause your `#run` method to clean up
84
- its resources and return. If you don't provide such a method, then the default
85
- behaviour of `#shutdown` is to send an exception to the running thread, which
86
- is somewhat brutal and prone to unpleasantness. If your service wants to
87
- cleanly exit of its own accord, it can also just return from the `#run` method,
88
- and the service will terminate without any fuss.
88
+ When your service needs to be stopped for one reason or another, `ServiceSkeleton`
89
+ needs to be able to tell your code to stop. By default, the thread that is
90
+ running your service will just be killed, which might be fine if your service
91
+ holds no state or persistent resources, but often that isn't the case.
92
+
93
+ If your code needs to stop gracefully, you should define a (thread-safe)
94
+ instance method, `#shutdown`, which does whatever is required to signal to
95
+ your service worker code that it is time to return from the `#run` method.
96
+ What that does, exactly, is up to you.
97
+
98
+ ```
99
+ class CustomShutdownService
100
+ include ServiceSkeleton
101
+
102
+ def run
103
+ until @shutdown do
104
+ puts "Hello, Service!"
105
+ sleep 1
106
+ end
107
+
108
+ puts "Shutting down gracefully..."
109
+ end
110
+
111
+ def shutdown
112
+ @shutdown = true
113
+ end
114
+ end
115
+ ```
116
+
117
+ To avoid the unpleasantness of a hung service, there is a limit on the amount
118
+ of time that `ServiceSkeleton` will wait for your service code to terminate.
119
+ This is, by default, five seconds, but you can modify that by defining a
120
+ `#shutdown_timeout` method, which returns a `Numeric`, to specify the number of
121
+ seconds that `ServiceSkeleton` should wait for termination.
122
+
123
+ ```
124
+ class SlowShutdownService
125
+ include ServiceSkeleton
126
+
127
+ def run
128
+ until @shutdown do
129
+ puts "Hello, Service!"
130
+ sleep 60
131
+ end
132
+ end
133
+
134
+ def shutdown
135
+ @shutdown = true
136
+ end
137
+
138
+ def shutdown_timeout
139
+ # We need an unusually long shutdown timeout for this service because
140
+ # the shutdown flag is only checked once a minute, which is much longer
141
+ # than the default shutdown period.
142
+ 90
143
+ end
144
+ end
145
+ ```
146
+
147
+ If your service code does not terminate before the timeout, the thread will be,
148
+ once again, unceremoniously killed.
89
149
 
90
150
 
91
151
  ### Exceptional Behaviour
92
152
 
93
153
  If your `#run` loop happens to raise an unhandled exception, it will be caught,
94
- logged, and the service will terminate. `ServiceSkeleton` believes in the
95
- value of "fail fast", and if an exception makes it all way out, then there's no
96
- telling what has gone wrong or if it can be reasonably recovered. The safest
97
- option is to terminate the service process entirely, and have your service
98
- supervisor start everything up again from scratch.
154
+ logged, and your service will be restarted. This involves instantiating a new
155
+ instance of your service class, and calling `#run` again.
156
+
157
+ In the event that the problem that caused the exception isn't transient, and
158
+ your service code keeps exiting (either by raising an exception, or the `#run`
159
+ method returning), the supervisor will, after a couple of retries, terminate
160
+ the whole process.
161
+
162
+ This allows for a *really* clean slate restart, by starting a whole new
163
+ process. Your process manager should handle automatically restarting the
164
+ process in a sensible manner.
99
165
 
100
166
 
101
167
  ## The Service Name
102
168
 
103
169
  Several aspects of a `ServiceSkeleton` service, including environment variable
104
- and metric names, expect the service's name as a prefix by default. The
105
- service name is derived from the name of the class that subclasses
106
- `ServiceSkeleton`, by converting the `CamelCase` class name into a `snake_case`
107
- service name. If the class name is in a namespace, that is included also, with
108
- the `::` turned into `_`.
170
+ and metric names, can incorporate the service's name, usually as a prefix. The
171
+ service name is derived from the name of the class that you provide to
172
+ `ServiceSkeleton::Runner.new`, by converting the `CamelCase` class name into a
173
+ `snake_case` service name. If the class name is in a namespace, that is
174
+ included also, with the `::` turned into `_`.
109
175
 
110
176
 
111
177
  ## Configuration
112
178
 
113
179
  Almost every service has a need for some amount of configuration. In keeping
114
- with the general principles of the [12 factor app](https://12factor.net), the
180
+ with the general principles of the [12 factor app](https://12factor.net),
115
181
  `ServiceSkeleton` takes configuration from the environment. However, we try to
116
182
  minimise the amount of manual effort you need to expend to make that happen,
117
183
  and provide configuration management as a first-class operation.
@@ -119,15 +185,17 @@ and provide configuration management as a first-class operation.
119
185
 
120
186
  ### Basic Configuration
121
187
 
122
- Every `ServiceSkeleton` has an instance method defined, called `config`, which
123
- returns an instance of `ServiceSkeleton::Config` (or some other class you
188
+ The `ServiceSkeleton` module defines an instance method, called `#config`, which
189
+ returns an instance of {ServiceSkeleton::Config} (or some other class you
124
190
  specify; more on that below), which provides access to the environment that was
125
191
  passed into the service object at instantiation time (ie the `ENV` in
126
- `MyService.new(ENV)`) via the `#[]` method. So, in a very simple
192
+ `ServiceSkeleton.new(MyService, ENV)`) via the `#[]` method. So, in a very simple
127
193
  application where you want to get the name of the thing to say hello to, it
128
194
  might look like this:
129
195
 
130
- class GenericHelloService < ServiceSkeleton
196
+ class GenericHelloService
197
+ include ServiceSkeleton
198
+
131
199
  def run
132
200
  loop do
133
201
  puts "Hello, #{config["RECIPIENT"]}!"
@@ -136,17 +204,21 @@ might look like this:
136
204
  end
137
205
  end
138
206
 
207
+ ServiceSkeleton::Runner.new(GenericHelloService, "RECIPIENT" => "Bob").start
208
+
209
+ This will print "Hello, Bob!" every second.
139
210
 
140
- ### Variable Declaration
211
+
212
+ ### Declaring Configuration Variables
141
213
 
142
214
  If your application has very minimal needs, it's possible that directly
143
- accessing the environment will be sufficient. However, you can (and probably
215
+ accessing the environment will be sufficient. However, you can (and usually
144
216
  should) declare your configuration variables in your service class, because
145
- that way you can get coerced values (numbers and booleans, rather than strings
146
- everywhere), range and format checking (say "the number must be between one and
147
- ten", or "the string must match this regex"), default values, and error
148
- reporting. You also get direct access to the configuration value as a method
149
- call on the `config` object.
217
+ that way you can get coerced values (numbers, booleans, lists, etc, rather than
218
+ just plain strings), range and format checking (say "the number must be an
219
+ integer between one and ten", or "the string must match this regex"), default
220
+ values, and error reporting. You also get direct access to the configuration
221
+ value as a method call on the `config` object.
150
222
 
151
223
  To declare configuration variables, simply call one of the "config declaration
152
224
  methods" (as listed in the `ServiceSkeleton::ConfigVariables` module) in your
@@ -154,20 +226,24 @@ class definition, and pass it an environment variable name (as a string or
154
226
  symbol) and any relevant configuration parameters (like a default, or a
155
227
  validity range, or whatever).
156
228
 
157
- When your service object is instantiated, the environment will be examined and
158
- the configuration setup. If any values are invalid (number out of range, etc)
159
- or missing (for any configuration variable that doesn't have a default), then
160
- an error will be logged and the service will not start.
229
+ When you run your service (via {ServiceSkeleton::Runner#new}), the environment
230
+ you pass in will be examined and the configuration initialised. If any values
231
+ are invalid (number out of range, etc) or missing (for any configuration
232
+ variable that doesn't have a default), then a
233
+ {ServiceSkeleton::InvalidEnvironmentError} exception will be raised and the
234
+ service will not start.
161
235
 
162
236
  During your service's execution, any time you need to access a configuration
163
237
  value, just call the matching method name (the all-lowercase version of the
164
- environment variable name) on `config`, and you'll get the value in your
165
- lap.
238
+ environment variable name, without the service name prefix) on `config`, and
239
+ you'll get the value in your lap.
166
240
 
167
241
  Here's a version of our generic greeter service, using declared configuration
168
242
  variables:
169
243
 
170
- class GenericHelloService < ServiceSkeleton
244
+ class GenericHelloService
245
+ include ServiceSkeleton
246
+
171
247
  string :RECIPIENT, matches: /\A\w+\z/
172
248
 
173
249
  def run
@@ -178,27 +254,51 @@ variables:
178
254
  end
179
255
  end
180
256
 
257
+ begin
258
+ ServiceSkeleton::Runner.new(GenericHelloService, ENV).run
259
+ rescue ServiceSkeleton::InvalidEnvironmentError => ex
260
+ $stderr.puts "Configuration error found: #{ex.message}"
261
+ exit 1
262
+ end
263
+
264
+ This service, if run without a `RECIPIENT` environment variable being available,
265
+ will exit with an error. If that isn't what you want, you can declare a
266
+ default for a config variable, like so:
267
+
268
+ class GenericHelloService
269
+ include ServiceSkeleton
270
+
271
+ string :RECIPIENT, matches: /\a\w+\z/, default: "Anonymous Coward"
272
+
273
+ # ...
274
+
275
+ *This* version will print "Hello, Anonymous Coward!" if no `RECIPIENT`
276
+ environment variable is available.
277
+
181
278
 
182
279
  ### Environment Variable Prefixes
183
280
 
184
281
  It's common for all (or almost all) of your environment variables to have a
185
- common prefix, usually named for your service, so it's easy to identify your
186
- service's configuration from anything else that's lying around. However, you
187
- don't want to have to use that prefix when accessing your `config` methods.
282
+ common prefix, usually named for your service, to distinguish your service's
283
+ configuration from any other environment variables lying around. However, to
284
+ save on typing, you don't want to have to use that prefix when accessing your
285
+ `config` methods.
188
286
 
189
287
  Enter: the service name prefix. Any of your environment variables whose name
190
288
  starts with [your service's name](#the-service-name) (matched
191
289
  case-insensitively) followed by an underscore will have that part of the
192
290
  environment variable name removed to determine the method name on `config`.
193
291
  The *original* environment variable name is still matched to a variable
194
- declaration, so, you need to declare the variable *with* the prefix, but the
195
- method name won't have it.
292
+ declaration, so, you need to declare the variable *with* the prefix, it is only
293
+ the method name on the `config` object that won't have the prefix.
196
294
 
197
295
  Using this environment variable prefix support, the `GenericHelloService` would
198
296
  have a (case-insensitive) prefix of `generic_hello_service_`. In that case,
199
297
  extending the above example a little more, you could do something like this:
200
298
 
201
- class GenericHelloService < ServiceSkeleton
299
+ class GenericHelloService
300
+ include ServiceSkeleton
301
+
202
302
  string :GENERIC_HELLO_SERVICE_RECIPIENT, matches: /\A\w+\z/
203
303
 
204
304
  def run
@@ -219,12 +319,14 @@ Sometimes your service will take configuration data that really, *really*
219
319
  shouldn't be available to subprocesses or anyone who manages to catch a
220
320
  sneak-peek at your service's environment. In that case, you can declare an
221
321
  environment variable as "sensitive", and after the configuration is parsed,
222
- that environment variable will be wiped from the environment.
322
+ that environment variable will be redacted from the environment.
223
323
 
224
324
  To declare an environment variable as "sensitive", simply pass the `sensitive`
225
325
  parameter, with a trueish value, to the variable declaration in your class:
226
326
 
227
- class DatabaseManager < ServiceSkeleton
327
+ class DatabaseManager
328
+ include ServiceSkeleton
329
+
228
330
  string :DB_PASSWORD, sensitive: true
229
331
 
230
332
  ...
@@ -232,17 +334,18 @@ parameter, with a trueish value, to the variable declaration in your class:
232
334
 
233
335
  > **NOTE**: The process environment can only be modified if you pass the real,
234
336
  > honest-to-goodness `ENV` object into `MyServiceClass.new(ENV)`. If you
235
- > provide a copy, or some other hash, that'll work *normally*, but if you have
236
- > sensitive variables, the service will log an error and refuse to start. This
237
- > avoids the problems of accidentally modifying global state if that would be
238
- > potentially bad (we assume you copied `ENV` for a reason) without leaving a
239
- > gaping security hole (sensitive data blindly passed into subprocesses that
240
- > you didn't expect).
337
+ > provide a copy of `ENV`, or some other hash entirely, that'll work if you
338
+ > don't have any sensitive variables declared, but the moment you declare a
339
+ > sensitive variable, passing in any hash other than `ENV` will cause the
340
+ > service to log an error and refuse to start. This avoids the problems of
341
+ > accidentally modifying global state if that would be potentially bad (we
342
+ > assume you copied `ENV` for a reason) without leaving a gaping security hole
343
+ > (sensitive data blindly passed into subprocesses that you didn't expect).
241
344
 
242
345
 
243
- ### Custom Configuration Class
346
+ ### Using a Custom Configuration Class
244
347
 
245
- Whilst we hope that `ServiceSkeleton::Config` class will be useful in most
348
+ Whilst we hope that {ServiceSkeleton::Config} will be useful in most
246
349
  situations, there are undoubtedly cases where the config management we provide
247
350
  won't be enough. In that case, you are encouraged to subclass
248
351
  `ServiceSkeleton::Config` and augment the standard interface with your own
@@ -258,7 +361,9 @@ class method in your service's class definition, like this:
258
361
  end
259
362
  end
260
363
 
261
- class MyService < ServiceSkeleton
364
+ class MyService
365
+ include ServiceSkeleton
366
+
262
367
  config_class MyServiceConfig
263
368
 
264
369
  def run
@@ -279,11 +384,11 @@ for you to use.
279
384
 
280
385
  ### What You Get
281
386
 
282
- Every instance of your service class (as subclassed from `ServiceSkeleton`)
283
- has a method named, uncreatively, `logger`. It is a (more-or-less) straight-up
284
- instance of the Ruby stdlib `Logger`, on which you can call all the usual
285
- methods (`#debug`, `#info`, `#warn`, `#error`, etc). By default, it sends all
286
- log messages to standard error.
387
+ Every instance of your service class has a method named, uncreatively,
388
+ `logger`. It is a (more-or-less) straight-up instance of the Ruby stdlib
389
+ `Logger`, on which you can call all the usual methods (`#debug`, `#info`,
390
+ `#warn`, `#error`, etc). By default, it sends all log messages to standard
391
+ error.
287
392
 
288
393
  When calling the logger, you really, *really* want to use the
289
394
  "progname+message-in-a-block" style of recording log messages, which looks like
@@ -299,22 +404,22 @@ wish to actively debug, based on log messages that are tagged with a specified
299
404
  progname. No more grovelling through thousands of lines of debug logging to
300
405
  find the One Useful Message.
301
406
 
302
- The `ServiceSkeleton` also provides built-in dynamic log level adjustment;
407
+ You also get, as part of this package, built-in dynamic log level adjustment;
303
408
  using Unix signals or the admin HTTP interface (if enabled), you can tell the
304
409
  logger to increase or decrease logging verbosity *without interrupting
305
410
  service*. We are truly living in the future.
306
411
 
307
- Finally, if you're a devotee of the ELK stack, we can automagically send log
308
- entries straight into logstash, rather than you having to do it in some
309
- more roundabout fashion.
412
+ Finally, if you're a devotee of the ELK stack, the logger can automagically
413
+ send log entries straight into logstash, rather than you having to do it in
414
+ some more roundabout fashion.
310
415
 
311
416
 
312
417
  ### Logging Configuration
313
418
 
314
419
  The logger automatically sets its configuration from, you guessed it, the
315
420
  environment. The following environment variables are recognised by the logger.
316
- All are all-uppercase, and the `<SERVICENAME>_` portion is the all-uppercase
317
- [service name](#the-service-name).
421
+ All environment variable names are all-uppercase, and the `<SERVICENAME>_`
422
+ portion is the all-uppercase [service name](#the-service-name).
318
423
 
319
424
  * **`<SERVICENAME>_LOG_LEVEL`** (default: `"INFO"`) -- the minimum severity of
320
425
  log messages which will be emitted by the logger.
@@ -325,21 +430,21 @@ All are all-uppercase, and the `_` portion is the all-uppercase
325
430
 
326
431
  If you wish to change the severity level for a single progname, you can
327
432
  override the default log level for messages with a specific progname, by
328
- specifying one or more "progname severities" separated by commas. A progname
329
- severity looks like this:
433
+ specifying one or more "progname/severity" pairs, separated by commas. A
434
+ progname/severity pair looks like this:
330
435
 
331
436
  <progname>=<severity>
332
437
 
333
438
  To make things even more fun, if `<progname>` looks like a regular expression
334
439
  (starts with `/` or `%r{`, and ends with `/` or `}` plus optional flag
335
440
  characters), then all log messages with prognames *matching* the specified
336
- regexp will have that severity applied. First match wins. The default is
441
+ regex will have that severity applied. First match wins. The default is
337
442
  still specified as a bare severity name, and the default can only be set
338
443
  once.
339
444
 
340
- That's a lot to take in, so here's an example which sets the default to `INFO`,
341
- debugs the `buggy` progname, and only emits errors for messages with the
342
- (case-insensitive) string `noisy` in their progname:
445
+ That's a lot to take in, so here's an example which sets the default to
446
+ `INFO`, debugs the `buggy` progname, and only emits errors for messages with
447
+ the (case-insensitive) string `noisy` in their progname:
343
448
 
344
449
  INFO,buggy=DEBUG,/noisy/i=ERROR
345
450
 
@@ -365,8 +470,8 @@ All are all-uppercase, and the `_` portion is the all-uppercase
365
470
  where log messages aren't automatically timestamped, then you can use this to
366
471
  get them back.
367
472
 
368
- * **`<SERVICENAME>_LOG_FILE`** (string; default: `"/dev/stderr`) -- the file to
369
- which log messages are written. The default, to send messages to standard
473
+ * **`<SERVICENAME>_LOG_FILE`** (string; default: `"/dev/stderr"`) -- the file
474
+ to which log messages are written. The default, to send messages to standard
370
475
  error, is a good choice if you are using a supervisor system which captures
371
476
  service output to its own logging system, however if you are stuck without
372
477
  such niceties, you can specify a file on disk to log to instead.
@@ -401,7 +506,7 @@ All are all-uppercase, and the `_` portion is the all-uppercase
401
506
  ## Metrics
402
507
 
403
508
  Running a service without metrics is like trying to fly a fighter jet whilst
404
- blindfolded. Everything seems to be going OK until you slam into the side of a
509
+ blindfolded: everything seems to be going OK until you slam into the side of a
405
510
  mountain you never saw coming. For that reason, `ServiceSkeleton` provides a
406
511
  Prometheus-based metrics registry, a bunch of default process-level metrics, an
407
512
  optional HTTP metrics server, and simple integration with [the Prometheus ruby
@@ -413,45 +518,43 @@ easy as possible to instrument the heck out of your service.
413
518
  ### Defining and Using Metrics
414
519
 
415
520
  All the metrics you want to use within your service need to be registered
416
- before use. This is typically done in the `#run` method, before entering the
417
- infinite loop.
521
+ before use. This is done via class methods, similar to declaring environment
522
+ variables.
418
523
 
419
524
  To register a metric, use one of the standard metric registration methods from
420
525
  [Prometheus::Client::Registry](https://www.rubydoc.info/gems/prometheus-client/0.8.0/Prometheus/Client/Registry)
421
- (`#counter`, `#gauge`, `#histogram`, `#summary`, or `#register`) on the `metrics`
422
- object to create or register the metric.
526
+ (`counter`, `gauge`, `histogram`, `summary`) or `metric` (equivalent
527
+ to the `register` method of `Prometheus::Client::Registry) in your class
528
+ definition to register the metric for use.
423
529
 
424
530
  In our generic greeter service we've been using as an example so far, you might
425
531
  like to define a metric to count how many greetings have been sent. You'd define
426
532
  such a metric like this:
427
533
 
428
- class GenericHelloService < ServiceSkeleton
534
+ class GenericHelloService
535
+ include ServiceSkeleton
536
+
429
537
  string :GENERIC_HELLO_SERVICE_RECIPIENT, matches: /\A\w+\z/
430
538
 
431
- def run
432
- metrics.counter(:greetings_total, "How many greetings we have sent")
539
+ counter :greetings_total, docstring: "How many greetings we have sent", labels: %i{recipient}
433
540
 
434
- loop do
435
- puts "Hello, #{config.recipient}!"
436
- sleep 1
437
- end
438
- end
439
- end
541
+ # ...
542
+
543
+ When it comes time to actually *use* the metrics you have created, you access
544
+ them as methods on the `metrics` method in your service worker instance. Thus,
545
+ to increment our greeting counter, you simply do:
440
546
 
441
- When it comes time to actually *use* the metrics you have created, it's typical
442
- to keep a copy of the metric object laying around, or call `metrics.get`. However,
443
- we make it easier to access your metrics, by defining a method named for the metric
444
- on `metrics`. Thus, to increment our greeting counter, you can simply do:
547
+ class GenericHelloService
548
+ include ServiceSkeleton
445
549
 
446
- class GenericHelloService < ServiceSkeleton
447
550
  string :GENERIC_HELLO_SERVICE_RECIPIENT, matches: /\A\w+\z/
448
551
 
449
- def run
450
- metrics.counter(:greetings_total, "How many greetings we have sent")
552
+ counter :greetings_total, docstring: "How many greetings we have sent", labels: %i{recipient}
451
553
 
554
+ def run
452
555
  loop do
453
556
  puts "Hello, #{config.recipient}!"
454
- metrics.greetings_total.increment(recipient: config.recipient)
557
+ metrics.greetings_total.increment(labels: { recipient: config.recipient })
455
558
  sleep 1
456
559
  end
457
560
  end
@@ -462,15 +565,17 @@ any metrics you define which have the [service name](#the-service-name) as a
462
565
  prefix will have that prefix (and the immediately-subsequent underscore) removed
463
566
  before defining the metric accessor method, which keeps typing to a minimum:
464
567
 
465
- class GenericHelloService < ServiceSkeleton
568
+ class GenericHelloService
569
+ include ServiceSkeleton
570
+
466
571
  string :GENERIC_HELLO_SERVICE_RECIPIENT, matches: /\A\w+\z/
467
572
 
468
- def run
469
- metrics.counter(:generic_hello_service_greetings_total, "How many greetings we have sent")
573
+ counter :generic_hello_service_greetings_total, docstring: "How many greetings we have sent", labels: %i{recipient}
470
574
 
575
+ def run
471
576
  loop do
472
577
  puts "Hello, #{config.recipient}!"
473
- metrics.greetings_total.increment(recipient: config.recipient)
578
+ metrics.greetings_total.increment(labels: { recipient: config.recipient })
474
579
  sleep 1
475
580
  end
476
581
  end
@@ -499,7 +604,7 @@ all-uppercase, and the `_` portion is the all-uppercase version
499
604
  of [the service name](#the-service-name).
500
605
 
501
606
  * **`<SERVICENAME>_METRICS_PORT`** (integer; range 1..65535; default: `""`) --
502
- if set to a non-empty integer which is a valid port number (`1` to `65535`,
607
+ if set to an integer which is a valid port number (`1` to `65535`,
503
608
  inclusive), an HTTP server will be started which will respond to a request to
504
609
  `/metrics` with a Prometheus-compatible dump of time series data.
505
610
 
@@ -517,13 +622,15 @@ behaviours for common signals.
517
622
 
518
623
  ### Default Signals
519
624
 
520
- When the `#run` method on your service instance is called, the following
521
- signals will be hooked with the following behaviour:
625
+ When the `#run` method on a `ServiceSkeleton::Runner` instance is called, the
626
+ following signals will be hooked, and will perform the described action when
627
+ that signal is received:
522
628
 
523
629
  * **`SIGUSR1`** -- increase the default minimum severity for messages which
524
- will be emitted by the logger. The default severity only applies to log
525
- messages whose progname does not match a "progname specifier" (see "[Logging
526
- Configuration](#logging-configuration)").
630
+ will be emitted by the logger (`FATAL` -> `ERROR` -> `WARN` -> `INFO` ->
631
+ `DEBUG`). The default severity only applies to log messages whose progname
632
+ does not match a "progname/severity" pair (see [Logging
633
+ Configuration](#logging-configuration)).
527
634
 
528
635
  * **`SIGUSR2`** -- decrease the default minimum severity for messages which
529
636
  will be emitted by the logger.
@@ -531,7 +638,7 @@ signals will be hooked with the following behaviour:
531
638
  * **`SIGHUP`** -- close and reopen the log file, if logging to a file on disk.
532
639
  Because of the `ServiceSkeleton`'s default log rotation policy, this shouldn't
533
640
  ordinarily be required, but if you've turned off the default log rotation,
534
- you may need tis.
641
+ you may need this.
535
642
 
536
643
  * **`SIGQUIT`** -- dump a *whooooooole* lot of debugging information to
537
644
  standard error, including memory allocation summaries and stack traces of all
@@ -541,38 +648,43 @@ signals will be hooked with the following behaviour:
541
648
 
542
649
  * **`SIGINT`** / **`SIGTERM`** -- ask the service to gracefully stop running.
543
650
  It will call your service's `#shutdown` method to ask it to stop what it's
544
- doing and exit. If the signal is sent twice, your run method will be
545
- summarily terminated and everything will be terminated quickly. As usual, if
546
- a service process needs to be whacked completely and utterly *right now*,
547
- `SIGKILL` is what you want to use.
651
+ doing and exit. If the signal is sent a second time, the service will be
652
+ summarily terminated as soon as practical, without being given the
653
+ opportunity to gracefully release resources. As usual, if a service process
654
+ needs to be whacked completely and utterly *right now*, `SIGKILL` is what you
655
+ want to use.
548
656
 
549
657
 
550
658
  ### Hooking Signals
551
659
 
552
660
  In addition to the above default signal dispositions, you can also hook signals
553
661
  yourself for whatever purpose you desire. This is typically done in your
554
- `#run` method, before entering the infinite loop.
662
+ `#run` method, before entering the main service loop.
555
663
 
556
664
  To hook a signal, just call `hook_signal` with a signal specification and a
557
- block of code to execute when the signal fires. You can even hook the same
558
- signal more than once, because the signal handlers that `SkeletonService` uses
559
- chain to other signal handlers. As an example, if you want to print "oof!"
560
- every time the `SIGCONT` signal is received, you'd do something like this:
665
+ block of code to execute when the signal fires in your class definition. You
666
+ can even hook the same signal more than once, because the signal handlers that
667
+ `ServiceSkeleton` uses chain to other signal handlers. As an example, if you
668
+ want to print "oof!" every time the `SIGCONT` signal is received, you'd do
669
+ something like this:
561
670
 
562
- class MyService < ServiceSkeleton
563
- def run
564
- hook_signal("CONT") { puts "oof!" }
671
+ class MyService
672
+ include ServiceSkeleton
565
673
 
674
+ hook_signal("CONT") { puts "oof!" }
675
+
676
+ def run
566
677
  loop { sleep }
567
678
  end
568
679
  end
569
680
 
681
+ The code in the block will be executed in the context of the service worker
682
+ instance that is running at the time the signal is received. You are
683
+ responsible for ensuring that whatever your handler does is concurrency-safe.
684
+
570
685
  When the service is shutdown, all signal handlers will be automatically
571
686
  unhooked, which saves you having to do it yourself.
572
687
 
573
- > **NOTE**: You can define a maximum of 256 signal hooks in a single service,
574
- > including the default signal hooks.
575
-
576
688
 
577
689
  ## HTTP Admin Interface
578
690
 
@@ -584,9 +696,9 @@ different?
584
696
  ### HTTP Admin Configuration
585
697
 
586
698
  In the spirit of "secure by default", you must explicitly enable the HTTP admin
587
- interface, and it requires authentication. To do that, use the following
588
- environment variables, where `<SERVICENAME>_` is the all-uppercase version of
589
- [the service name](#the-service-name).
699
+ interface, and configure an authentication method. To do that, use the
700
+ following environment variables, where `<SERVICENAME>_` is the all-uppercase
701
+ version of [the service name](#the-service-name).
590
702
 
591
703
  * **`<SERVICENAME>_HTTP_ADMIN_PORT`** (integer; range 1..65535; default: `""`)
592
704
  -- if set to a valid port number (`1` to `65535` inclusive), the HTTP admin
@@ -610,7 +722,11 @@ environment variables, where `_` is the all-uppercase version of
610
722
  interface to be enabled.
611
723
 
612
724
 
613
- ### HTTP Admin Features
725
+ ### HTTP Admin Usage
726
+
727
+ The HTTP admin interface provides both an interactive, browser-based mode,
728
+ as well as a RESTful interface, which should, in general, provide equivalent
729
+ functionality.
614
730
 
615
731
  * Visiting the service's `IP address:port` in a web browser will bring up an HTML
616
732
  interface showing all the features that are available. Usage should
@@ -635,7 +751,8 @@ conduct](CODE_OF_CONDUCT.md).
635
751
  Unless otherwise stated, everything in this repo is covered by the following
636
752
  copyright notice:
637
753
 
638
- Copyright (C) 2018 Civilized Discourse Construction Kit, Inc.
754
+ Copyright (C) 2018, 2019 Civilized Discourse Construction Kit, Inc.
755
+ Copyright (C) 2019, 2020 Matt Palmer
639
756
 
640
757
  This program is free software: you can redistribute it and/or modify it
641
758
  under the terms of the GNU General Public License version 3, as