-
Notifications
You must be signed in to change notification settings - Fork 66
Expand file tree
/
Copy pathMonitoring.hs
More file actions
302 lines (277 loc) · 11.2 KB
/
Monitoring.hs
File metadata and controls
302 lines (277 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
{-# LANGUAGE CPP, OverloadedStrings #-}
-- | This module provides remote monitoring of a running process over
-- HTTP. It can be used to run an HTTP server that provides both a
-- web-based user interface and a machine-readable API (e.g. JSON.)
-- The former can be used by a human to get an overview of what the
-- program is doing and the latter can be used by automated monitoring
-- tools.
--
-- Typical usage is to start the monitoring server at program startup
--
-- > main = do
-- > forkServer "localhost" 8000
-- > ...
--
-- and then periodically check the stats using a web browser or a
-- command line tool (e.g. curl)
--
-- > $ curl -H "Accept: application/json" http://localhost:8000/
module System.Remote.Monitoring
(
-- * Required configuration
-- $configuration
-- * Security considerations
-- $security
-- * REST API
-- $api
-- * The monitoring server
Server
, serverAsync
, serverThreadId
, serverMetricStore
, forkServer
, forkServerNoHostname
, forkServerWith
, forkServerNoHostnameWith
-- * Defining metrics
-- $userdefined
, getCounter
, getGauge
, getLabel
, getDistribution
) where
import Control.Concurrent.Async (async, Async (asyncThreadId))
import Control.Concurrent (ThreadId)
import qualified Data.ByteString as S
import Data.Int (Int64)
import qualified Data.Text as T
import Data.Time.Clock.POSIX (getPOSIXTime)
import Prelude hiding (read)
import qualified System.Metrics as Metrics
import qualified System.Metrics.Counter as Counter
import qualified System.Metrics.Distribution as Distribution
import qualified System.Metrics.Gauge as Gauge
import qualified System.Metrics.Label as Label
import System.Remote.Snap
import Network.Socket (withSocketsDo)
-- $configuration
--
-- To make full use out of this module you must first enable GC
-- statistics collection in the run-time system. To enable GC
-- statistics collection, either run your program with
--
-- > +RTS -T
--
-- or compile it with
--
-- > -with-rtsopts=-T
--
-- The runtime overhead of @-T@ is very small so it's safe to always
-- leave it enabled.
-- $security
-- Be aware that if the server started by 'forkServer' is not bound to
-- \"localhost\" (or equivalent) anyone on the network can access the
-- monitoring server. Either make sure the network is secure or bind
-- the server to \"localhost\".
-- $api
-- To use the machine-readable REST API, send an HTTP GET request to
-- the host and port passed to 'forkServer'.
--
-- The API is versioned to allow for API evolution. This document is
-- for version 1. To ensure you're using this version, append @?v=1@
-- to your resource URLs. Omitting the version number will give you
-- the latest version of the API.
--
-- The following resources (i.e. URLs) are available:
--
-- [\/] JSON object containing all metrics. Metrics are stored as
-- nested objects, with one new object layer per \".\" in the metric
-- name (see example below.) Content types: \"text\/html\" (default),
-- \"application\/json\"
--
-- [\/\<namespace\>/\<metric\>] JSON object for a single metric. The
-- metric name is created by converting all \"\/\" to \".\". Example:
-- \"\/foo\/bar\" corresponds to the metric \"foo.bar\". Content
-- types: \"application\/json\"
--
-- Each metric is returned as an object containing a @type@ field. Available types
-- are:
--
-- * \"c\" - 'Counter.Counter'
--
-- * \"g\" - 'Gauge.Gauge'
--
-- * \"l\" - 'Label.Label'
--
-- * \"d\" - 'Distribution.Distribution'
--
-- In addition to the @type@ field, there are metric specific fields:
--
-- * Counters, gauges, and labels: the @val@ field contains the
-- actual value (i.e. an integer or a string).
--
-- * Distributions: the @mean@, @variance@, @count@, @sum@, @min@,
-- and @max@ fields contain their statistical equivalents.
--
-- Example of a response containing the metrics \"myapp.visitors\" and
-- \"myapp.args\":
--
-- > {
-- > "myapp": {
-- > "visitors": {
-- > "val": 10,
-- > "type": "c"
-- > },
-- > "args": {
-- > "val": "--a-flag",
-- > "type": "l"
-- > }
-- > }
-- > }
-- $userdefined
-- The monitoring server can store and serve integer-valued counters
-- and gauges, string-valued labels, and statistical distributions. A
-- counter is a monotonically increasing value (e.g. TCP connections
-- established since program start.) A gauge is a variable value (e.g.
-- the current number of concurrent connections.) A label is a
-- free-form string value (e.g. exporting the command line arguments
-- or host name.) A distribution is a statistic summary of events
-- (e.g. processing time per request.) Each metric is associated with
-- a name, which is used when it is displayed in the UI or returned in
-- a JSON object.
--
-- Metrics share the same namespace so it's not possible to create
-- e.g. a counter and a gauge with the same. Attempting to do so will
-- result in an 'error'.
--
-- To create and use a counter, simply call 'getCounter' to create it
-- and then call e.g. 'Counter.inc' or 'Counter.add' to modify its
-- value. Example:
--
-- > main = do
-- > handle <- forkServer "localhost" 8000
-- > counter <- getCounter "iterations" handle
-- > let loop n = do
-- > inc counter
-- > loop
-- > loop
--
-- To create a gauge, use 'getGauge' instead of 'getCounter' and then
-- call e.g. 'System.Remote.Gauge.set'. Similar for the other metric
-- types.
--
-- It's also possible to register metrics directly using the
-- @System.Metrics@ module in the ekg-core package. This gives you a
-- bit more control over how metric values are retrieved.
------------------------------------------------------------------------
-- * The monitoring server
-- | A handle that can be used to control the monitoring server.
-- Created by 'forkServer'.
data Server = Server {
-- | The thread ID of the server. You can kill the server by
-- killing this thread (i.e. by throwing it an asynchronous
-- exception.)
serverAsync :: {-# UNPACK #-} !(Async ())
-- | The metric store associated with the server. If you want to
-- add metric to the default store created by 'forkServer' you
-- need to use this function to retrieve it.
, serverMetricStore :: {-# UNPACK #-} !Metrics.Store
}
serverThreadId :: Server -> ThreadId
serverThreadId = asyncThreadId . serverAsync
-- | Like 'forkServerWith', but creates a default metric store with
-- some predefined metrics. The predefined metrics are those given in
-- 'System.Metrics.registerGcMetrics'.
forkServer :: S.ByteString -- ^ Host to listen on (e.g. \"localhost\")
-> Int -- ^ Port to listen on (e.g. 8000)
-> IO Server
forkServer host port = do
store <- Metrics.newStore
Metrics.registerGcMetrics store
forkServerMaybeHostnameWith store (Just host) port
-- | Create a server with prefined metrics from
-- 'System.Metrics.registerGcMetrics', listening on all interfaces.
-- If you are running EKG on a private network (including virtual
-- private network), it may be appropriate to bind to all interfaces,
-- not only localhost.
forkServerNoHostname :: Int -- ^ Port to listen on (e.g. 8000)
-> IO Server
forkServerNoHostname port = do
store <- Metrics.newStore
Metrics.registerGcMetrics store
forkServerMaybeHostnameWith store Nothing port
-- | Start an HTTP server in a new thread. The server replies to GET
-- requests to the given host and port. The host argument can be
-- either a numeric network address (dotted quad for IPv4,
-- colon-separated hex for IPv6) or a hostname (e.g. \"localhost\".)
-- The client can control the Content-Type used in responses by
-- setting the Accept header. At the moment two content types are
-- available: \"application\/json\" and \"text\/html\".
--
-- Registers the following counter, used by the UI:
--
-- [@ekg.server_time_ms@] The server time when the sample was taken,
-- in milliseconds.
--
-- Note that this function, unlike 'forkServer', doesn't register any
-- other predefined metrics. This allows other libraries to create and
-- provide a metric store for use with this library. If the metric
-- store isn't created by you and the creator doesn't register the
-- metrics registered by 'forkServer', you might want to register them
-- yourself.
forkServerWith :: Metrics.Store -- ^ Metric store
-> S.ByteString -- ^ Host to listen on (e.g. \"localhost\")
-> Int -- ^ Port to listen on (e.g. 8000)
-> IO Server
forkServerWith store host port =
forkServerMaybeHostnameWith store (Just host) port
-- | Start an HTTP server in a new thread, with the specified metrics
-- store, listening on all interfaces. Other than accepting requests
-- to any hostname, this is the same as `forkServerWith`.
forkServerNoHostnameWith :: Metrics.Store -- ^ Metric store
-> Int -- ^ Port to listen on (e.g. 8000)
-> IO Server
forkServerNoHostnameWith store port =
forkServerMaybeHostnameWith store Nothing port
forkServerMaybeHostnameWith :: Metrics.Store -- ^ Metric store
-> Maybe S.ByteString -- ^ Host to listen on (e.g. \"localhost\")
-> Int -- ^ Port to listen on (e.g. 8000)
-> IO Server
forkServerMaybeHostnameWith store host port = do
Metrics.registerCounter "ekg.server_timestamp_ms" getTimeMs store
a <- async $ withSocketsDo $ startServer store host port
return $! Server a store
where
getTimeMs :: IO Int64
getTimeMs = (round . (* 1000)) `fmap` getPOSIXTime
------------------------------------------------------------------------
-- * Defining metrics
-- | Return a new, zero-initialized counter associated with the given
-- name and server. Multiple calls to 'getCounter' with the same
-- arguments will result in an 'error'.
getCounter :: T.Text -- ^ Counter name
-> Server -- ^ Server that will serve the counter
-> IO Counter.Counter
getCounter name server = Metrics.createCounter name (serverMetricStore server)
-- | Return a new, zero-initialized gauge associated with the given
-- name and server. Multiple calls to 'getGauge' with the same
-- arguments will result in an 'error'.
getGauge :: T.Text -- ^ Gauge name
-> Server -- ^ Server that will serve the gauge
-> IO Gauge.Gauge
getGauge name server = Metrics.createGauge name (serverMetricStore server)
-- | Return a new, empty label associated with the given name and
-- server. Multiple calls to 'getLabel' with the same arguments will
-- result in an 'error'.
getLabel :: T.Text -- ^ Label name
-> Server -- ^ Server that will serve the label
-> IO Label.Label
getLabel name server = Metrics.createLabel name (serverMetricStore server)
-- | Return a new distribution associated with the given name and
-- server. Multiple calls to 'getDistribution' with the same arguments
-- will result in an 'error'.
getDistribution :: T.Text -- ^ Distribution name
-> Server -- ^ Server that will serve the distribution
-> IO Distribution.Distribution
getDistribution name server =
Metrics.createDistribution name (serverMetricStore server)