changed CHANGELOG.md
 
@@ -7,6 +7,51 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
> Major version zero (0.y.z) is for initial development. Anything MAY change at
8
8
any time. The public API SHOULD NOT be considered stable.
9
9
10
+ ## [v0.5.0] - 2022-05-32
11
+
12
+ Breaking changes for three functions in the `Mobius.Exports` module:
13
+
14
+ 1. `Mobius.Exports.series/4`
15
+ 1. `Mobius.Exports.metrics/4`
16
+ 1. `Mobius.Exports.plot/4`
17
+
18
+ If you are now directly calling these functions in code you're safe to upgrade.
19
+
20
+ The first two use to return either `{:ok, results}` or `{:error, reason}` but
21
+ now they will only return their result. For `Mobius.Exports.series/4` the return
22
+ value is now `[integer()]` and for `Mobius.Exports.metrics/4` the return type is
23
+ now `[Mobius.metric()]`. `Mobius.Exports.plot/4` still returns `:ok` on success,
24
+ but can now return `{:error, UnsupportedMetricError.t()}`.
25
+
26
+ ### Changed
27
+
28
+ * `Mobius.Exports.series/4` return type was
29
+ `{:ok, [integer()]} | {:error, UnsupportedMetricError.t()}` and now is
30
+ `[integer()]`.
31
+ * `Mobius.Exports.metrics/4` return type was
32
+ `{:ok, [Mobius.metric()]} | {:error, UnsupportedMetricError.t()}` and now is
33
+ `[Mobius.metric()]`
34
+ * `Mobius.Exports.plot/4` was just `:ok` but now is
35
+ `:ok | {:error, UnsupportedMetricError.t()}`
36
+
37
+ ### Added
38
+
39
+ * `Mobius.RemoteReporter` behaviour to allow for reporting metrics to a remote
40
+ server.
41
+ * Add `:remote_reporter` and `:remote_report_interval` options to the
42
+ `Mobius.arg()` type.
43
+ * Super for specify which summery metric you want to export. (@ewildgoose)
44
+ * Support for summary metrics types in some exports. (@ewildgoose)
45
+ * Add standard deviation calculation to the summary metric type. (@ewildgoose)
46
+ * New `Mobius.Exports.export_metric_type()` that allows for specifying the
47
+ summary metric type.
48
+
49
+ ### Misc
50
+
51
+ * Update `ex_doc` to `v.0.28.4`
52
+ * Update `telemetry` to `v1.1.0`
53
+ * Fix up typos (@kianmeng)
54
+
10
55
## [v0.4.0] - 2022-03-25
11
56
12
57
### Changed
 
@@ -60,96 +105,97 @@ basic maintenance like dependency updates and documentation improvements
60
105
61
106
### Added
62
107
63
- - Create, save, and extract tar files that contain metric data, see
108
+ * Create, save, and extract tar files that contain metric data, see
64
109
`Mobius.Bundles` and `Mobius.make_bundle/2` for more information.
65
- - `Mobius.filter_metrics/3` to filter for desired metrics to enable the
110
+ * `Mobius.filter_metrics/3` to filter for desired metrics to enable the
66
111
metrics to be consumed externally (@ewildgoose)
67
- - `Mobius.save/1` to manually save the state of the metric data for Mobius
112
+ * `Mobius.save/1` to manually save the state of the metric data for Mobius
68
113
(@ewildgoose)
69
- - `:autosave_interval` option to Mobius to enable a saving data at the given
114
+ * `:autosave_interval` option to Mobius to enable a saving data at the given
70
115
interval (@ewildgoose)
71
116
72
117
### Fixes
73
118
74
- - Unit conversion not working correctly (@ewildgoose)
75
- - Error handling for when the `:persistence_path` is missing (@ewildgoose)
76
- - Error handling when there is no data to plot (@ewildgoose)
77
- - Crash when plotting an array of identical values (@ewildgoose)
78
- - Correct off by one error when plotting (@ewildgoose)
119
+ * Unit conversion not working correctly (@ewildgoose)
120
+ * Error handling for when the `:persistence_path` is missing (@ewildgoose)
121
+ * Error handling when there is no data to plot (@ewildgoose)
122
+ * Crash when plotting an array of identical values (@ewildgoose)
123
+ * Correct off by one error when plotting (@ewildgoose)
79
124
80
125
## [v0.3.6] - 2022-01-25
81
126
82
127
### Added
83
128
84
- - Support for `Telemetry.Metrics.Summary` metric type
129
+ * Support for `Telemetry.Metrics.Summary` metric type
85
130
86
131
## [v0.3.5] - 2021-12-2
87
132
88
133
### Fixes
89
134
90
- - Fix crash when initializing metrics table when the ETS file cannot be read (@jfcloutier)
135
+ * Fix crash when initializing metrics table when the ETS file cannot be read (@jfcloutier)
91
136
92
137
## [v0.3.4] - 2021-11-15
93
138
94
139
### Fixes
95
140
96
- - Fix crash when a history file is unreadable during initialization (@mdwaud)
141
+ * Fix crash when a history file is unreadable during initialization (@mdwaud)
97
142
98
143
## [v0.3.3] - 2021-10-20
99
144
100
145
### Fixes
101
146
102
- - Not able to pass a path for persistence that contains non-existing sub
147
+ * Not able to pass a path for persistence that contains non-existing sub
103
148
directories. Thank you [LostKobrakai](https://github.com/LostKobrakai).
104
149
105
150
## [v0.3.2] - 2021-09-22
106
151
107
152
### Added
108
153
109
- - Support for `Telemetry.Metrics.Sum` type
110
- - Support for filtering CSV records by type with `:type` option
154
+ * Support for `Telemetry.Metrics.Sum` type
155
+ * Support for filtering CSV records by type with `:type` option
111
156
112
157
## [v0.3.1] - 2021-09-08
113
158
114
159
### Added
115
160
116
- - Plot over the last `x` seconds via the `:last` plot option
117
- - Plot from an absolute time via the `:from` plot option
118
- - Plot to an absolute time via the `:to` plot option
119
- - Print or save metric time series via `Mobius.to_csv/3`
120
- - Remove tracking a metric by dropping it from the metric list passed to Mobius
161
+ * Plot over the last `x` seconds via the `:last` plot option
162
+ * Plot from an absolute time via the `:from` plot option
163
+ * Plot to an absolute time via the `:to` plot option
164
+ * Print or save metric time series via `Mobius.to_csv/3`
165
+ * Remove tracking a metric by dropping it from the metric list passed to Mobius
121
166
122
167
### Changed
123
168
124
- - `Mobius.plot/3` will only show the last 3 minutes of data by default
169
+ * `Mobius.plot/3` will only show the last 3 minutes of data by default
125
170
126
171
## [v0.3.0] - 2021-8-19
127
172
128
173
### Changed
129
174
130
- - Deleted `Mobius.Charts` module. The functions in this module are now located
175
+ * Deleted `Mobius.Charts` module. The functions in this module are now located
131
176
in the `Mobius` module.
132
177
133
178
### Removed
134
179
135
- - Support for specifying resolutions.
180
+ * Support for specifying resolutions.
136
181
137
182
## [v0.2.0] - 2021-8-03
138
183
139
184
### Added
140
185
141
- - `Mobius.Charts` module
142
- - Persistence of historical information on graceful shutdown
143
- - Ability to specify time resolutions for plots
186
+ * `Mobius.Charts` module
187
+ * Persistence of historical information on graceful shutdown
188
+ * Ability to specify time resolutions for plots
144
189
145
190
### Changed
146
191
147
- - Move `Moblus.plot/0` and `Mobius.info/0` to `Mobius.Charts` module
192
+ * Move `Moblus.plot/0` and `Mobius.info/0` to `Mobius.Charts` module
148
193
149
194
## v0.1.0 - 2021-7-16
150
195
151
196
Initial release!
152
197
198
+ [v0.5.0]: https://github.com/mattludwigs/mobius/compare/v0.4.0...v0.5.0
153
199
[v0.4.0]: https://github.com/mattludwigs/mobius/compare/v0.3.7...v0.4.0
154
200
[v0.3.7]: https://github.com/mattludwigs/mobius/compare/v0.3.6...v0.3.7
155
201
[v0.3.6]: https://github.com/mattludwigs/mobius/compare/v0.3.5...v0.3.6
changed README.md
 
@@ -1,6 +1,6 @@
1
1
# Mobius
2
2
3
- [![CircleCI](https://circleci.com/gh/mattludwigs/mobius/tree/main.svg?style=svg)](https://circleci.com/gh/mattludwigs/mobius/tree/main)
3
+ [![CircleCI](https://circleci.com/gh/mobius-home/mobius/tree/main.svg?style=svg)](https://circleci.com/gh/mobius-home/mobius/tree/main)
4
4
5
5
![Mobius](assets/mobius-name.png)
6
6
 
@@ -11,7 +11,7 @@ Library for localized telemetry metrics
11
11
```elixir
12
12
def deps do
13
13
[
14
- {:mobius, "~> 0.4.0"}
14
+ {:mobius, "~> 0.5.0"}
15
15
]
16
16
end
17
17
```
 
@@ -123,7 +123,6 @@ def start(_type, _args) do
123
123
end
124
124
```
125
125
126
-
127
126
### Exporting data
128
127
129
128
The `Mobius.Exports` module provide functions for exporting the data in a couple
 
@@ -151,3 +150,11 @@ most useful for preparing metrics to send off to another system. To parse
151
150
the binary format you can use `Mobius.Exports.parse_mbf/1`.
152
151
153
152
For each of these you can see the `Mobius.Exports` module for more details.
153
+
154
+ ### Report metrics to a remote server
155
+
156
+ Mobius allows sending metrics to a remote server. You can do this by passing the
157
+ `:remote_reporter` option to Mobius. This is a module that implements the
158
+ `Mobius.RemoteReporter` behaviour. Optionally, you can pass the
159
+ `:remote_report_interval` option to specify how often to report metrics, by
160
+ default this is every 1 minute.
changed hex_metadata.config
 
@@ -3,15 +3,19 @@
3
3
{<<"description">>,<<"Local metrics library">>}.
4
4
{<<"elixir">>,<<"~> 1.11">>}.
5
5
{<<"files">>,
6
- [<<"lib">>,<<"lib/mobius.ex">>,<<"lib/mobius">>,<<"lib/mobius/events.ex">>,
7
- <<"lib/mobius/registry.ex">>,<<"lib/mobius/asciichart.ex">>,
8
- <<"lib/mobius/rrd.ex">>,<<"lib/mobius/metrics_table">>,
9
- <<"lib/mobius/metrics_table/monitor.ex">>,<<"lib/mobius/exports.ex">>,
10
- <<"lib/mobius/scraper.ex">>,<<"lib/mobius/exports">>,
6
+ [<<"lib">>,<<"lib/mobius.ex">>,<<"lib/mobius">>,<<"lib/mobius/scraper.ex">>,
7
+ <<"lib/mobius/exports.ex">>,<<"lib/mobius/metrics_table">>,
8
+ <<"lib/mobius/metrics_table/monitor.ex">>,<<"lib/mobius/exceptions.ex">>,
9
+ <<"lib/mobius/remote_reporter.ex">>,<<"lib/mobius/auto_save.ex">>,
10
+ <<"lib/mobius/remote_reporter_server.ex">>,
11
+ <<"lib/mobius/remote_reporters">>,
12
+ <<"lib/mobius/remote_reporters/logger_reporter.ex">>,
13
+ <<"lib/mobius/exports">>,<<"lib/mobius/exports/metrics.ex">>,
14
+ <<"lib/mobius/exports/csv.ex">>,
11
15
<<"lib/mobius/exports/mobius_binary_format.ex">>,
12
- <<"lib/mobius/exports/metrics.ex">>,<<"lib/mobius/exports/csv.ex">>,
13
- <<"lib/mobius/metrics_table.ex">>,<<"lib/mobius/auto_save.ex">>,
14
- <<"lib/mobius/summary.ex">>,<<"lib/mobius/exceptions.ex">>,
16
+ <<"lib/mobius/asciichart.ex">>,<<"lib/mobius/rrd.ex">>,
17
+ <<"lib/mobius/events.ex">>,<<"lib/mobius/metrics_table.ex">>,
18
+ <<"lib/mobius/summary.ex">>,<<"lib/mobius/registry.ex">>,
15
19
<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"CHANGELOG.md">>]}.
16
20
{<<"licenses">>,[<<"Apache-2.0">>]}.
17
21
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/mattludwigs/mobius">>}]}.
 
@@ -32,4 +36,4 @@
32
36
{<<"optional">>,false},
33
37
{<<"repository">>,<<"hexpm">>},
34
38
{<<"requirement">>,<<"~> 0.4.0">>}]]}.
35
- {<<"version">>,<<"0.4.0">>}.
39
+ {<<"version">>,<<"0.5.0">>}.
changed lib/mobius.ex
 
@@ -5,7 +5,7 @@ defmodule Mobius do
5
5
6
6
use Supervisor
7
7
8
- alias Mobius.{MetricsTable, Scraper, Summary}
8
+ alias Mobius.{MetricsTable, RemoteReporter, Scraper, Summary}
9
9
10
10
alias Telemetry.Metrics
11
11
 
@@ -29,6 +29,8 @@ defmodule Mobius do
29
29
| {:metrics, [Metrics.t()]}
30
30
| {:persistence_dir, binary()}
31
31
| {:database, Mobius.RRD.t()}
32
+ | {:remote_reporter, RemoteReporter.t() | {RemoteReporter.t(), term()}}
33
+ | {:remote_report_interval, non_neg_integer()}
32
34
33
35
@typedoc """
34
36
The name of the Mobius instance
 
@@ -69,7 +71,11 @@ defmodule Mobius do
69
71
Start Mobius
70
72
"""
71
73
def start_link(args) do
72
- Supervisor.start_link(__MODULE__, ensure_args(args), name: __MODULE__.Supervisor)
74
+ Supervisor.start_link(__MODULE__, ensure_args(args), name: name(args[:mobius_instance]))
75
+ end
76
+
77
+ defp name(instance) do
78
+ Module.concat(__MODULE__.Supervisor, instance)
73
79
end
74
80
75
81
@impl Supervisor
 
@@ -92,6 +98,7 @@ defmodule Mobius do
92
98
{Mobius.Scraper, args}
93
99
]
94
100
|> maybe_enable_autosave(args)
101
+ |> maybe_start_remote_reporter(args)
95
102
96
103
Supervisor.init(children, strategy: :one_for_one)
97
104
 
@@ -128,6 +135,27 @@ defmodule Mobius do
128
135
end
129
136
end
130
137
138
+ defp maybe_start_remote_reporter(children, args) do
139
+ case args[:remote_reporter] do
140
+ nil ->
141
+ children
142
+
143
+ _config ->
144
+ children ++ [{Mobius.RemoteReporterServer, make_remote_report_server_args(args)}]
145
+ end
146
+ end
147
+
148
+ defp make_remote_report_server_args(mobius_args) do
149
+ reporter = Keyword.fetch!(mobius_args, :remote_reporter)
150
+ report_interval = mobius_args[:remote_report_interval]
151
+
152
+ [
153
+ reporter: reporter,
154
+ report_interval: report_interval,
155
+ mobius_instance: mobius_args[:mobius_instance]
156
+ ]
157
+ end
158
+
131
159
@doc """
132
160
Get the current metric information
changed lib/mobius/exports.ex
 
@@ -15,7 +15,8 @@ defmodule Mobius.Exports do
15
15
`mbf/1`.
16
16
"""
17
17
18
- alias Mobius.Exports.{MobiusBinaryFormat, UnsupportedMetricError}
18
+ alias Mobius.Asciichart
19
+ alias Mobius.Exports.{CSV, Metrics, MobiusBinaryFormat, UnsupportedMetricError}
19
20
20
21
@typedoc """
21
22
Options to use when exporting time series metric data
 
@@ -45,6 +46,14 @@ defmodule Mobius.Exports do
45
46
| {:headers, boolean()}
46
47
| {:iodevice, IO.device()}
47
48
49
+ @typedoc """
50
+ Metric types that can be exported
51
+
52
+ By default you can try to export any `Mobius.metric_type()`, but for then
53
+ summary metric type you can specify which summary type you want to export.
54
+ """
55
+ @type export_metric_type() :: Mobius.metric_type() | {:summary, atom()}
56
+
48
57
@doc """
49
58
Generate a CSV for the metric
50
59
 
@@ -62,32 +71,28 @@ defmodule Mobius.Exports do
62
71
:ok = Mobius.Exports.csv("vm.memory.total", :last_value, %{}, iodevice: file)
63
72
```
64
73
"""
65
- @spec csv(binary(), Mobius.metric_type(), map(), [csv_export_opt()]) ::
74
+ @spec csv(binary(), export_metric_type(), map(), [csv_export_opt()]) ::
66
75
:ok | {:ok, binary()} | {:error, UnsupportedMetricError.t()}
67
- def csv(metric_name, type, tags, opts \\ []) do
68
- case get_metrics(metric_name, type, tags, opts) do
69
- {:ok, metrics} ->
70
- export_opts = build_exporter_opts(metric_name, type, tags, opts)
71
- Mobius.Exports.CSV.export_metrics(metrics, export_opts)
76
+ def csv(metric_name, type, tags, opts \\ [])
72
77
73
- error ->
74
- error
75
- end
78
+ def csv(_metric_name, :summary, _tags, _opts) do
79
+ {:error, UnsupportedMetricError.exception(metric_type: :summary)}
80
+ end
81
+
82
+ def csv(metric_name, type, tags, opts) do
83
+ metrics = get_metrics(metric_name, type, tags, opts)
84
+ export_opts = build_exporter_opts(metric_name, type, tags, opts)
85
+ CSV.export_metrics(metrics, export_opts)
76
86
end
77
87
78
88
@doc """
79
89
Generates a series that contains the value of the metric
80
90
"""
81
- @spec series(String.t(), Mobius.metric_type(), map(), [export_opt()]) ::
82
- {:ok, [integer()]} | {:error, UnsupportedMetricError.t()}
91
+ @spec series(String.t(), export_metric_type(), map(), [export_opt()]) :: [integer()]
83
92
def series(metric_name, type, tags, opts \\ []) do
84
- case get_metrics(metric_name, type, tags, opts) do
85
- {:ok, metrics} ->
86
- {:ok, Enum.map(metrics, fn metric -> metric.value end)}
87
-
88
- error ->
89
- error
90
- end
93
+ metric_name
94
+ |> get_metrics(type, tags, opts)
95
+ |> Enum.map(& &1.value)
91
96
end
92
97
93
98
@doc """
 
@@ -126,17 +131,19 @@ defmodule Mobius.Exports do
126
131
```elixir
127
132
Mobius.Exports.metrics("vm.memory.total", :last_value, %{}, last: {2, :hour})
128
133
```
134
+
135
+ Retrieving summary data can be performed by specifying the type: :summary - however, this returns
136
+ value data in the form of a map, which cannot be plotted or csv exported. To reduce the output to
137
+ a single metric value, use the form: {:summary, :summary_metric}
138
+
139
+ ```elixir
140
+ Mobius.Exports.metrics("vm.memory.total", {:summary, :average}, %{}, last: {2, :hour})
141
+ ```
129
142
"""
130
143
@spec metrics(Mobius.metric_name(), Mobius.metric_type(), map(), [export_opt()] | keyword()) ::
131
- {:ok, [Mobius.metric()]} | {:error, UnsupportedMetricError.t()}
132
- def metrics(metric_name, type, tags, opts \\ [])
133
-
134
- def metrics(_metric_name, :summary, _tags, _opts) do
135
- {:error, UnsupportedMetricError.exception(metric_type: :summary)}
136
- end
137
-
138
- def metrics(metric_name, type, tags, opts) do
139
- {:ok, Mobius.Exports.Metrics.export(metric_name, type, tags, opts)}
144
+ [Mobius.metric()]
145
+ def metrics(metric_name, type, tags, opts \\ []) do
146
+ Metrics.export(metric_name, type, tags, opts)
140
147
end
141
148
142
149
defp get_metrics(metric_name, type, tags, opts) do
 
@@ -190,28 +197,45 @@ defmodule Mobius.Exports do
190
197
```elixir
191
198
Mobius.Export.plot("vm.memory.total", :last_value, %{}, last: {2, :hour})
192
199
```
193
- """
194
- @spec plot(Mobius.metric_name(), Mobius.metric_type(), map(), [export_opt()]) :: :ok
195
- def plot(metric_name, type, tags \\ %{}, opts \\ []) do
196
- with {:ok, series} <- series(metric_name, type, tags, opts),
197
- {:ok, plot} <- Mobius.Asciichart.plot(series, height: 12) do
198
- chart = [
199
- "\t\t",
200
- IO.ANSI.yellow(),
201
- "Metric Name: ",
202
- metric_name,
203
- IO.ANSI.reset(),
204
- ", ",
205
- IO.ANSI.cyan(),
206
- "Tags: #{inspect(tags)}",
207
- IO.ANSI.reset(),
208
- "\n\n",
209
- plot
210
- ]
211
200
212
- IO.puts(chart)
213
- else
214
- error -> error
201
+ Retrieving summary data can be performed by specifying type of the form:
202
+ {:summary, :summary_metric}
203
+
204
+ ```elixir
205
+ Mobius.Exports.metrics("vm.memory.total", {:summary, :average}, %{}, last: {2, :hour})
206
+ ```
207
+ """
208
+ @spec plot(Mobius.metric_name(), export_metric_type(), map(), [export_opt()]) ::
209
+ :ok | {:error, UnsupportedMetricError.t()}
210
+ def plot(metric_name, type, tags \\ %{}, opts \\ [])
211
+
212
+ def plot(_metric_name, :summary, _tags, _opts) do
213
+ {:error, UnsupportedMetricError.exception(metric_type: :summary)}
214
+ end
215
+
216
+ def plot(metric_name, type, tags, opts) do
217
+ series = series(metric_name, type, tags, opts)
218
+
219
+ case Asciichart.plot(series, height: 12) do
220
+ {:ok, plot} ->
221
+ chart = [
222
+ "\t\t",
223
+ IO.ANSI.yellow(),
224
+ "Metric Name: ",
225
+ metric_name,
226
+ IO.ANSI.reset(),
227
+ ", ",
228
+ IO.ANSI.cyan(),
229
+ "Tags: #{inspect(tags)}",
230
+ IO.ANSI.reset(),
231
+ "\n\n",
232
+ plot
233
+ ]
234
+
235
+ IO.puts(chart)
236
+
237
+ error ->
238
+ error
215
239
end
216
240
end
changed lib/mobius/exports/metrics.ex
 
@@ -3,7 +3,7 @@ defmodule Mobius.Exports.Metrics do
3
3
4
4
# Module for exporting metrics
5
5
6
- alias Mobius.{Exports, Scraper}
6
+ alias Mobius.{Exports, Scraper, Summary}
7
7
8
8
@doc """
9
9
Export metrics
 
@@ -43,7 +43,25 @@ defmodule Mobius.Exports.Metrics do
43
43
rows
44
44
end
45
45
46
+ defp filter_metrics_for_metric(metrics, metric_name, :summary, tags) do
47
+ do_filter_metrics_for_metric(metrics, metric_name, :summary, tags)
48
+ |> Enum.map(fn metric ->
49
+ %{metric | value: metric.value |> Summary.calculate()}
50
+ end)
51
+ end
52
+
53
+ defp filter_metrics_for_metric(metrics, metric_name, {:summary, summary_metric}, tags) do
54
+ do_filter_metrics_for_metric(metrics, metric_name, :summary, tags)
55
+ |> Enum.map(fn metric ->
56
+ %{metric | value: metric.value |> Summary.calculate() |> Map.get(summary_metric)}
57
+ end)
58
+ end
59
+
46
60
defp filter_metrics_for_metric(metrics, metric_name, type, tags) do
61
+ do_filter_metrics_for_metric(metrics, metric_name, type, tags)
62
+ end
63
+
64
+ defp do_filter_metrics_for_metric(metrics, metric_name, type, tags) do
47
65
Enum.filter(metrics, fn metric ->
48
66
metric_name == metric.name && match?(^tags, metric.tags) && type == metric.type
49
67
end)
changed lib/mobius/exports/mobius_binary_format.ex
 
@@ -1,6 +1,8 @@
1
1
defmodule Mobius.Exports.MobiusBinaryFormat do
2
2
@moduledoc false
3
3
4
+ alias Mobius.Exports.MBFParseError
5
+
4
6
@format_version 1
5
7
6
8
@doc """
 
@@ -16,22 +18,20 @@ defmodule Mobius.Exports.MobiusBinaryFormat do
16
18
"""
17
19
@spec parse(binary()) :: {:ok, [Mobius.metric()]} | {:error, Mobius.Exports.MBFParseError.t()}
18
20
def parse(<<@format_version, metrics_bin::binary>>) do
19
- try do
20
- metrics = :erlang.binary_to_term(metrics_bin)
21
+ metrics = :erlang.binary_to_term(metrics_bin)
21
22
22
- if validate_metrics(metrics) do
23
- {:ok, metrics}
24
- else
25
- {:error, Mobius.Exports.MBFParseError.exception(:invalid_format)}
26
- end
27
- rescue
28
- ArgumentError ->
29
- {:error, Mobius.Exports.MBFParseError.exception(:corrupt)}
23
+ if validate_metrics(metrics) do
24
+ {:ok, metrics}
25
+ else
26
+ {:error, MBFParseError.exception(:invalid_format)}
30
27
end
28
+ rescue
29
+ ArgumentError ->
30
+ {:error, MBFParseError.exception(:corrupt)}
31
31
end
32
32
33
33
def parse(_other) do
34
- {:error, Mobius.Exports.MBFParseError.exception(:invalid_format)}
34
+ {:error, MBFParseError.exception(:invalid_format)}
35
35
end
36
36
37
37
defp validate_metrics(metrics) when is_list(metrics) do
added lib/mobius/remote_reporter.ex
 
@@ -0,0 +1,85 @@
1
+ defmodule Mobius.RemoteReporter do
2
+ @moduledoc """
3
+ Behaviour for modules that report mobius metrics to a remote server
4
+
5
+ A remote reporter allows mobius to report metrics to a remote server at some
6
+ configured interval.
7
+
8
+ Say we have a remote reporter who needs an API token to communicate with a
9
+ remote server. The implementation could look like:
10
+
11
+ ```elixir
12
+ defmodule MyRemoteReporter do
13
+ @behaviour Mobius.RemoteReporter
14
+
15
+ @impl Mobius.RemoteReporter
16
+ def init(args) do
17
+ token = Keyword.fetch!(args, :token)
18
+
19
+ {:ok, %{token: token}}
20
+ end
21
+
22
+ @impl Mobius.RemoteReporter
23
+ def handle_metrics(metrics, state) do
24
+ # ...send metrics
25
+ {:noreply, state}
26
+ end
27
+ end
28
+ ```
29
+
30
+ To use this implementation configure Mobius to report metrics every minute
31
+ (the default).
32
+
33
+ ```elixir
34
+ def start(_type, _args) do
35
+ metrics = [
36
+ Metrics.last_value("my.telemetry.event"),
37
+ ]
38
+
39
+ children = [
40
+ # ... other children ....
41
+ {Mobius,
42
+ metrics: metrics,
43
+ remote_reporter:
44
+ {MyRemoteReporter,
45
+ token: "s3curity"},
46
+ remote_report_interval: 60_000}
47
+ # ... other children ....
48
+ ]
49
+
50
+ opts = [strategy: :one_for_one, name: MyApp.Supervisor]
51
+ Supervisor.start_link(children, opts)
52
+ end
53
+ ```
54
+
55
+ If you are okay with the one minute reports you do not have to provide the
56
+ `:remote_report_interval` option.
57
+ """
58
+
59
+ @typedoc """
60
+ Module that implements the `Mobius.RemoteReporter` behaviour
61
+ """
62
+ @type t() :: module()
63
+
64
+ @doc """
65
+ Initialize the reporter
66
+
67
+ This callback will receive any configured arguments. These are specific to the
68
+ reporter implementation but some options might be tokens, host names, ports,
69
+ etc.
70
+ """
71
+ @callback init(opts :: term()) :: {:ok, state :: term()}
72
+
73
+ @doc """
74
+ Handle when metrics are ready to be reported
75
+
76
+ This callback will receive a list of metrics and can preform any operation it
77
+ is designed to do.
78
+
79
+ The report will only receive the metric from the last report time until the
80
+ current time, so the implementation does not need to worry about querying
81
+ only the newest metrics.
82
+ """
83
+ @callback handle_metrics([Mobius.metric()], state :: term()) ::
84
+ {:noreply, state :: term()}
85
+ end
added lib/mobius/remote_reporter_server.ex
 
@@ -0,0 +1,91 @@
1
+ defmodule Mobius.RemoteReporterServer do
2
+ @moduledoc false
3
+
4
+ use GenServer
5
+
6
+ require Logger
7
+
8
+ alias Mobius.{RemoteReporter, Scraper}
9
+
10
+ @typedoc """
11
+ Arguments to the client server
12
+ """
13
+ @type arg() ::
14
+ {:reporter, RemoteReporter.t() | {RemoteReporter.t(), term()}}
15
+ | {:report_interval, non_neg_integer()}
16
+ | {:mobius_instance, Mobius.instance()}
17
+
18
+ @doc """
19
+ Start the client server
20
+ """
21
+ @spec start_link([arg()]) :: GenServer.on_start()
22
+ def start_link(args) do
23
+ instance = Keyword.fetch!(args, :mobius_instance)
24
+
25
+ GenServer.start_link(__MODULE__, args, name: name(instance))
26
+ end
27
+
28
+ defp name(mobius_instance) do
29
+ Module.concat(__MODULE__, mobius_instance)
30
+ end
31
+
32
+ @impl GenServer
33
+ def init(args) do
34
+ instance = args[:mobius_instance] || :mobius
35
+ {reporter, reporter_args} = get_reporter(args)
36
+ {:ok, state} = reporter.init(reporter_args)
37
+ report_interval = args[:report_interval] || 60_000
38
+
39
+ timer_ref = Process.send_after(self(), :report, report_interval)
40
+
41
+ {:ok,
42
+ %{
43
+ reporter: reporter,
44
+ reporter_state: state,
45
+ report_interval: report_interval,
46
+ interval_ref: timer_ref,
47
+ next_query_from: nil,
48
+ mobius: instance
49
+ }}
50
+ end
51
+
52
+ defp get_reporter(args) do
53
+ case Keyword.fetch!(args, :reporter) do
54
+ {reporter, _client_args} = return when is_atom(reporter) -> return
55
+ reporter when is_atom(reporter) -> {reporter, []}
56
+ end
57
+ end
58
+
59
+ @impl GenServer
60
+ def handle_info(:report, state) do
61
+ {from, to} = get_query_window(state)
62
+ records = Scraper.all(state.mobius, from: from, to: to)
63
+ new_timer_ref = Process.send_after(self(), :report, state.report_interval)
64
+
65
+ case state.reporter.handle_metrics(records, state.reporter_state) do
66
+ {:noreply, new_state} ->
67
+ {:noreply,
68
+ %{
69
+ state
70
+ | reporter_state: new_state,
71
+ next_query_from: to + 1,
72
+ interval_ref: new_timer_ref
73
+ }}
74
+ end
75
+ end
76
+
77
+ def get_query_window(%{next_query_from: nil} = state) do
78
+ now = now()
79
+ subtract = div(state.report_interval, 1000)
80
+
81
+ {now - subtract, now}
82
+ end
83
+
84
+ def get_query_window(state) do
85
+ {state.next_query_from, now()}
86
+ end
87
+
88
+ defp now() do
89
+ DateTime.to_unix(DateTime.utc_now(), :second)
90
+ end
91
+ end
added lib/mobius/remote_reporters/logger_reporter.ex
 
@@ -0,0 +1,49 @@
1
+ defmodule Mobius.RemoteReporters.LoggerReporter do
2
+ @moduledoc """
3
+ Example remote reporter that logs the first and last metric
4
+
5
+ This logger is used in the example application.
6
+ """
7
+
8
+ @behaviour Mobius.RemoteReporter
9
+
10
+ require Logger
11
+
12
+ @impl Mobius.RemoteReporter
13
+ def init(_) do
14
+ {:ok, nil}
15
+ end
16
+
17
+ @impl Mobius.RemoteReporter
18
+ def handle_metrics(metrics, state) do
19
+ groups =
20
+ Enum.group_by(metrics, fn %{name: name, tags: tags, type: type} -> {name, type, tags} end)
21
+
22
+ out =
23
+ Enum.reduce(groups, "", fn {{name, type, tags}, grouped_metrics}, str ->
24
+ first = List.first(grouped_metrics)
25
+ last = List.last(grouped_metrics)
26
+
27
+ str <>
28
+ """
29
+ #{name}, #{inspect(type)}, #{inspect(tags)}
30
+
31
+ First: #{inspect(first.timestamp)}: #{inspect(first.value)}
32
+ Last: #{inspect(last.timestamp)}: #{inspect(last.value)}
33
+
34
+ """
35
+ end)
36
+
37
+ Logger.info("""
38
+
39
+ ======
40
+
41
+ Mobius LoggerReporter:
42
+
43
+ #{out}
44
+ ======
45
+ """)
46
+
47
+ {:noreply, state}
48
+ end
49
+ end
changed lib/mobius/summary.ex
 
@@ -4,7 +4,7 @@ defmodule Mobius.Summary do
4
4
@typedoc """
5
5
Calculated summary statistics
6
6
"""
7
- @type t() :: %{min: integer(), max: integer(), average: integer()}
7
+ @type t() :: %{min: integer(), max: integer(), average: float(), std_dev: float()}
8
8
9
9
@typedoc """
10
10
A data type to store snapshot information about a summary in order
 
@@ -14,6 +14,7 @@ defmodule Mobius.Summary do
14
14
min: integer(),
15
15
max: integer(),
16
16
accumulated: integer(),
17
+ accumulated_sqrd: integer(),
17
18
reports: non_neg_integer()
18
19
}
19
20
 
@@ -22,18 +23,25 @@ defmodule Mobius.Summary do
22
23
"""
23
24
@spec new(integer()) :: data()
24
25
def new(metric_value) do
25
- %{min: metric_value, max: metric_value, accumulated: metric_value, reports: 1}
26
+ %{
27
+ min: metric_value,
28
+ max: metric_value,
29
+ accumulated: metric_value,
30
+ accumulated_sqrd: metric_value * metric_value,
31
+ reports: 1
32
+ }
26
33
end
27
34
28
35
@doc """
29
36
Update a summary `data()` with new information based of a metric value
30
37
"""
31
- @spec update(data(), non_neg_integer()) :: data()
38
+ @spec update(data(), integer()) :: data()
32
39
def update(summary_data, new_metric_value) do
33
40
%{
34
41
min: min(summary_data.min, new_metric_value),
35
42
max: max(summary_data.max, new_metric_value),
36
43
accumulated: summary_data.accumulated + new_metric_value,
44
+ accumulated_sqrd: summary_data.accumulated_sqrd + new_metric_value * new_metric_value,
37
45
reports: summary_data.reports + 1
38
46
}
39
47
end
 
@@ -46,7 +54,17 @@ defmodule Mobius.Summary do
46
54
%{
47
55
min: summary_data.min,
48
56
max: summary_data.max,
49
- average: round(summary_data.accumulated / summary_data.reports)
57
+ average: summary_data.accumulated / summary_data.reports,
58
+ std_dev:
59
+ std_dev(summary_data.accumulated, summary_data.accumulated_sqrd, summary_data.reports)
50
60
}
51
61
end
62
+
63
+ defp std_dev(_sum, _sum_sqrd, 1), do: 0
64
+
65
+ # Naive algorithm. See Wikipedia
66
+ defp std_dev(sum, sum_sqrd, n) do
67
+ ((sum_sqrd - sum * sum / n) / (n - 1))
68
+ |> :math.sqrt()
69
+ end
52
70
end
changed mix.exs
 
@@ -1,7 +1,7 @@
1
1
defmodule Mobius.MixProject do
2
2
use Mix.Project
3
3
4
- @version "0.4.0"
4
+ @version "0.5.0"
5
5
6
6
def project do
7
7
[
 
@@ -42,7 +42,7 @@ defmodule Mobius.MixProject do
42
42
extras: ["README.md", "CHANGELOG.md"],
43
43
main: "readme",
44
44
source_ref: "v#{@version}",
45
- source_url: "https://github.com/mattludwigs/mobius",
45
+ source_url: "https://github.com/mobius-home/mobius",
46
46
skip_undefined_reference_warnings_on: ["CHANGELOG.md"],
47
47
assets: "assets",
48
48
logo: "assets/m.png"
 
@@ -62,7 +62,7 @@ defmodule Mobius.MixProject do
62
62
63
63
defp dialyzer() do
64
64
[
65
- flags: [:unmatched_returns, :error_handling, :race_conditions],
65
+ flags: [:unmatched_returns, :error_handling],
66
66
plt_add_apps: [:eex, :mix]
67
67
]
68
68
end