diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a561e4..0a2e44f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: matrix: include: - pair: - elixir: '1.14' + elixir: '1.15' otp: '24.3' postgres: '12.13-alpine' exclude_tags: 'lite' diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..f772ede --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 The Oban Team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/README.md b/README.md index afe51f8..ddcc0c1 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,159 @@ -# ObanMet +# Oban Met -### Rationale +

+ + Hex Version + -* Charts are helpful for monitoring health and troubleshooting from within Oban Web -* Not all apps can, or will, run an extra timeseries or metrics database -* State counts are driven by Postgres, but counting large tables is extremely slow; this requires something novel + + Hex Docs + -#### Setup + + CI Status + -* Oban.Met is a new package that's a dependency of ObanWeb and ObanPro, but could stand alone -* Oban.Met listens for [:oban, :supervisor, :start] events and starts a local ets table for each distinct instance + + Apache 2 License + +

-### Recording + -* Telemetry events for inserting, fetching, pruning, staging, etc. are recorded as counts -* State data is recorded as counts with labels for queue, worker, node, as well as a the "old" state for tracking -* Telemetry events for job execution are recorded as histograms, with the same labels -* Local collectors broadcast updates periodically, every 1s or so, using pubsub +Met is a distributed, compacting, multidimensional, telemetry-powered time series datastore for +Oban that requires no configurion. It gathers data for queues, job counts, execution metrics, +active crontabs, historic metrics, and more. -### Collecting +Met powers the charts and runtime details shown in the [Oban Web][web] dashboard. -* Oban.Web nodes collect broadcasted metrics and store them in ETS with a local timestamp -* Collected data is periodically compacted by time, e.g. keep per-second data for 5 minutes, per-minute data for 2 hours, and so on -* Counts (state, queue, etc) have periodic "keyframes" to ensure consistency from the database -* Counts between keyframes apply deltas for each state, e.g. executing +1, +1, -1 -* Before shutdown the collector hands off the full database (this could get plopped into a db table instead, not sure) -* (There are some critical details missing here) +[web]: https://github.com/oban-bg/oban_web -### Querying +## Features -* Oban.Met provides a few flexible functions to extract counts and summaries from stored data -* Counts include subgrouping (this is a detail of how I'd like to chart, not critical) -* Summaries pull from histograms, they are aggregates (min/max/p50/p95/p99) over a period of time with a particular resultion -* Counts and summaries may be filtered by one or more workers/queues/states/nodes -* There isn't some query language, it's not freeform, just some keyword options for flexibility +- **🤖 Autonomous** - Supervises a collection of autonomous modules that dynamically start and stop alongside + Oban instances without any code changes. -### Charting +- **🎩 Distributed** - Metrics are shared between all connected nodes via pubsub. Leadership is used + to restrict expensive operations, such as performing counts, to a single node. -* Not there yet. Maybe it will be an off-the-shelf package, but it's more likely to be html/svg driven by LV +- **📼 Recorded** - Telemetry events and scraped data are stored in-memory as time series data. + Values are stored as either gauges or space efficient "sketches". + +- **🪐 Multidimensional** - Metrics are stored with labels such as `node`, `queue`, `worker`, etc. + that can be filtered and grouped dynamically at runtime. + +- **🗜️ Compacting** - Time series values are periodically compacted into larger windows of time to + save space and optimize querying historic data. Compaction periods use safe defaults, but are + configurable. + +- **✏️ Estimating** - In supporting systems (Postgres), count queries use optimized estimates + automatically for tables with a large number of jobs. + +- **🔎 Queryable** - Historic metrics may be filtered and grouped by any label, sliced by + arbitrary time intervals, and numeric values aggregated at dynamic percentiles (e.g. P50, P99) + without pre-computed histogram buckets. + +- **🤝 Handoff** - Ephemeral data storage via data replication with handoff between nodes. All nodes have a + shared view of the cluster's data and new nodes are caught up when they come online. + +## Installation + +Oban Met is included with Oban Web and manual installation is only necessary in hybrid +environments (separate Web and Worker nodes). + +To receive metrics from non-web nodes in a system with separate "web" and "worker" applications +you must explicitly include `oban_met` as a dependency for "workers". + +```elixir +{:oban_met, "~> 1.0"} +``` + +## Usage + +No configuration is necessary and Oban Met will start automatically in a typical application. A +variety of options are provided for more complex or nuanced usage. + +### Auto Start + +Supervised Met instances start automatically along with Oban instances unless Oban is in testing +mode. You can disable auto-starting globally with application configuration: + +```elixir +config :oban_met, auto_start: false +``` + +Then, start instances as a child directly within your Oban app's plugins: + +```elixir +plugins: [ + Oban.Met, + ... +] +``` + +### Customizing Estimates + +Options for internal `Oban.Met` processes can be overridden from the plugin specification. Most +options are internal and not meant to be overridden, but one particularly useful option to tune is +the `estimate_limit`. The `estimate_limit` determines at which point state/queue counts switch +from using an accurate `count(*)` call to a much more efficient, but less accurate, estiamte +function. + +The default limit is a conservative 50k, which may be too low for systems with insert spikes. This +declares an override to set the limit to 200k: + +```elixir +{Oban.Met, reporter: [estimate_limit: 200_000]} +``` + +### Explicit Migrations + +Met will create the necessary estimate function automatically when possible. The migration isn't +necessary under normal circumstances, but is provided to avoid permission issues or allow full +control over database changes. + +```bash +mix ecto.gen.migration add_oban_met +``` + +Open the generated migration and delegate the `up/0` and `down/0` functions to +`Oban.Met.Migration`: + +```elixir +defmodule MyApp.Repo.Migrations.AddObanMet do + use Ecto.Migration + + def up, do: Oban.Met.Migration.up() + def down, do: Oban.Met.Migration.down() +end +``` + +Then, after disabling auto-start, configure the reporter not to auto-migrate if you run the +explicit migration: + +```elixir +{Oban.Met, reporter: [auto_migrate: false]} +``` + + + +## Contributing + +To run the test suite you must have PostgreSQL 12+. Once dependencies are installed, setup the +databases and run necessary migrations: + +```bash +mix test.setup +``` + +## Community + +There are a few places to connect and communicate with other Oban users: + +- Ask questions and discuss *#oban* on the [Elixir Forum][forum] +- [Request an invitation][invite] and join the *#oban* channel on Slack +- Learn about bug reports and upcoming features in the [issue tracker][issues] + +[invite]: https://elixir-slack.community/ +[forum]: https://elixirforum.com/ +[issues]: https://github.com/sorentwo/oban/issues diff --git a/guides/introduction/installation.md b/guides/introduction/installation.md deleted file mode 100644 index fddeec4..0000000 --- a/guides/introduction/installation.md +++ /dev/null @@ -1,75 +0,0 @@ -# Installation - -## Usage in Worker Only Nodes - -To receive metrics from non-web nodes in a system with separate "web" and "worker" applications you must explicitly include oban_met as a dependency for "workers". - -```elixir -# mix.exs -defp deps do - [ - {:oban_met, "~> 0.1", repo: :oban}, - ... -``` - -## Auto Start - -Supervised Met instances start automatically along with Oban instances unless Oban is in testing -mode. You can disable auto-starting globally with application configuration: - -```elixir -config :oban_met, auto_start: false -``` - -Then, start instances as a child directly within your Oban app's plugins: - -```elixir -plugins: [ - Oban.Met, - ... -] -``` - -## Customizing Estimates - -Options for internal `Oban.Met` processes can be overridden from the plugin specification. Most -options are internal and not meant to be overridden, but one particularly useful option to tune is -the `estimate_limit`. The `estimate_limit` determines at which point state/queue counts switch -from using an accurate `count(*)` call to a much more efficient, but less accurate, estiamte -function. - -The default limit is a conservative 50k, which may be too low for systems with insert spikes. This -declares an override to set the limit to 200k: - -```elixir -{Oban.Met, reporter: [estimate_limit: 200_000]} -``` - -## Explicit Migrations - -Met will create the necessary estimate function automatically when possible. The migration isn't -necessary under normal circumstances, but is provided to avoid permission issues or allow full -control over database changes. - -```bash -mix ecto.gen.migration add_oban_met -``` - -Open the generated migration and delegate the `up/0` and `down/0` functions to -`Oban.Met.Migration`: - -```elixir -defmodule MyApp.Repo.Migrations.AddObanMet do - use Ecto.Migration - - def up, do: Oban.Met.Migration.up() - def down, do: Oban.Met.Migration.down() -end -``` - -Then, after disabling auto-start, configure the reporter not to auto-migrate if you run the -explicit migration: - -```elixir -{Oban.Met, reporter: [auto_migrate: false]} -``` diff --git a/guides/introduction/overview.md b/guides/introduction/overview.md deleted file mode 100644 index d84820c..0000000 --- a/guides/introduction/overview.md +++ /dev/null @@ -1,26 +0,0 @@ -# Overview - -🔮 Oban.Met supervises a collection of autonomous modules for in-memory, distributed time-series -data with zero-configuration. `Oban.Web` relies on `Met` for queue gossip, detailed job counts, -and historic metrics. - -Metrics are gathered and managed automatically for applications using Oban Web. See -[installation](installation.md) for use in a worker-only node. - -## Features - -### 📇 Metric Aggregation - -Telemetry powered metric tracking and aggregation with compaction. - -### 📰 Queue Reporting - -Periodic queue checking and reporting (replaces the deprecated `Gossip` plugin). - -### 👷‍♀️ Job Reporting - -Periodic counting and reporting with backoff (replaces `Stats` plugin) - -### 🎩 Distributed Leadership - -Leader backed distributed metric sharing with handoff between nodes. diff --git a/lib/met.ex b/lib/met.ex index 5a353f1..5033594 100644 --- a/lib/met.ex +++ b/lib/met.ex @@ -1,23 +1,10 @@ defmodule Oban.Met do - @moduledoc """ - Metric introspection for Oban. + @external_resource readme = Path.join([__DIR__, "../README.md"]) - `Oban.Met` supervises a collection of autonomous modules for in-memory, distributed time-series - data with zero-configuration. `Oban.Web` relies on `Met` for queue gossip, detailed job counts, - and historic metrics. - - ## Highlights - - * Telemetry powered execution tracking for time-series data that is replicated between nodes, - filterable by label, arbitrarily mergeable over windows of time, and compacted for longer - playback. - - * Centralized counting across queues and states with exponential backoff to minimize load and - data replication between nodes. - - * Ephemeral data storage via data replication with handoff between nodes. All nodes have a - shared view of the cluster's data and new nodes are caught up when they come online. - """ + @moduledoc readme + |> File.read!() + |> String.split("") + |> Enum.fetch!(1) use Supervisor diff --git a/mix.exs b/mix.exs index 6df8772..c449152 100644 --- a/mix.exs +++ b/mix.exs @@ -1,15 +1,15 @@ defmodule Oban.Met.MixProject do use Mix.Project - @version "0.1.11" + @source_url "https://github.com/oban-bg/oban_met" + @version "1.0.0-dev" def project do [ app: :oban_met, version: @version, - elixir: "~> 1.13", + elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), - prune_code_paths: false, start_permanent: Mix.env() == :prod, deps: deps(), docs: docs(), @@ -36,9 +36,6 @@ defmodule Oban.Met.MixProject do ] end - defp elixirc_paths(:test), do: ["lib", "test/support"] - defp elixirc_paths(_env), do: ["lib"] - def application do [ extra_applications: [:logger], @@ -47,85 +44,41 @@ defmodule Oban.Met.MixProject do ] end - def package do - [ - organization: "oban", - files: ~w(lib/met.ex lib/oban mix.exs), - licenses: ["Commercial"], - links: [] - ] - end - - defp aliases do - [ - release: [ - "cmd rm -r doc", - "cmd git tag v#{@version}", - "cmd git push", - "cmd git push --tags", - "hex.publish package --yes", - "lys.publish" - ], - "test.reset": ["ecto.drop --quiet", "test.setup"], - "test.setup": ["ecto.create --quiet", "ecto.migrate --quiet"], - "test.ci": [ - "format --check-formatted", - "deps.unlock --check-unused", - "credo", - "test --raise", - "dialyzer" - ] - ] - end + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_env), do: ["lib"] - defp deps do + defp package do [ - {:oban, "~> 2.15"}, - {:telemetry, "~> 1.1"}, - {:ecto_sqlite3, "~> 0.9", only: [:test, :dev]}, - {:postgrex, "~> 0.19", only: [:test, :dev]}, - {:stream_data, "~> 1.1", only: [:test, :dev]}, - {:benchee, "~> 1.3", only: [:test, :dev], runtime: false}, - {:credo, "~> 1.7", only: [:test, :dev], runtime: false}, - {:dialyxir, "~> 1.3", only: [:test, :dev], runtime: false}, - {:ex_doc, "~> 0.34", only: :dev, runtime: false}, - {:makeup_diff, "~> 0.1", only: :dev, runtime: false}, - {:lys_publish, "~> 0.1", only: :dev, path: "../lys_publish", optional: true, runtime: false} + maintainers: ["Parker Selbert"], + licenses: ["Apache-2.0"], + files: ~w(lib .formatter.exs mix.exs README* CHANGELOG* LICENSE*), + links: %{ + Website: "https://oban.pro", + Changelog: "#{@source_url}/blob/main/CHANGELOG.md", + GitHub: @source_url + } ] end defp docs do [ - main: "overview", + main: "Oban.Met", source_ref: "v#{@version}", formatters: ["html"], api_reference: false, extra_section: "GUIDES", extras: extras(), - groups_for_extras: groups_for_extras(), groups_for_modules: groups_for_modules(), - homepage_url: "/", - skip_undefined_reference_warnings_on: ["CHANGELOG.md"], - before_closing_body_tag: fn _ -> - """ - - """ - end + skip_undefined_reference_warnings_on: ["CHANGELOG.md"] ] end defp extras do [ - "guides/introduction/overview.md", - "guides/introduction/installation.md", "CHANGELOG.md": [filename: "changelog", title: "Changelog"] ] end - defp groups_for_extras do - [Introduction: ~r/guides\/introduction\/.?/] - end - defp groups_for_modules do [ Values: [ @@ -135,4 +88,38 @@ defmodule Oban.Met.MixProject do ] ] end + + defp deps do + [ + {:oban, "~> 2.18"}, + {:ecto_sqlite3, "~> 0.18", only: [:test, :dev]}, + {:postgrex, "~> 0.19", only: [:test, :dev]}, + {:stream_data, "~> 1.1", only: [:test, :dev]}, + {:benchee, "~> 1.3", only: [:test, :dev], runtime: false}, + {:credo, "~> 1.7", only: [:test, :dev], runtime: false}, + {:dialyxir, "~> 1.3", only: [:test, :dev], runtime: false}, + {:ex_doc, "~> 0.34", only: :dev, runtime: false}, + {:makeup_diff, "~> 0.1", only: :dev, runtime: false} + ] + end + + defp aliases do + [ + release: [ + "cmd git tag v#{@version} -f", + "cmd git push", + "cmd git push --tags", + "hex.publish --yes" + ], + "test.reset": ["ecto.drop --quiet", "test.setup"], + "test.setup": ["ecto.create --quiet", "ecto.migrate --quiet"], + "test.ci": [ + "format --check-formatted", + "deps.unlock --check-unused", + "credo", + "test --raise", + "dialyzer" + ] + ] + end end diff --git a/mix.lock b/mix.lock index 2276533..40a3509 100644 --- a/mix.lock +++ b/mix.lock @@ -2,30 +2,29 @@ "benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, - "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, - "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"}, - "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"}, - "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.17.5", "fbee5c17ff6afd8e9ded519b0abb363926c65d30b27577232bb066b2a79957b8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "3b54734d998cbd032ac59403c36acf4e019670e8b6ceef9c6c33d8986c4e9704"}, + "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, + "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.18.0", "dd25010d7b536bb00857f3bf47193f293619b0c30538bf912d4bd82861d25fba", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "e42ef8252e22c6f65e5421289f9e5ceffd1a4a7e01cb2818d5eab78205d3cce7"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, "exqlite": {:hex, :exqlite, "0.27.1", "73fc0b3dc3b058a77a2b3771f82a6af2ddcf370b069906968a34083d2ffd2884", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "79ef5756451cfb022e8013e1ed00d0f8f7d1333c19502c394dc16b15cfb4e9b4"}, - "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, - "hex_core": {:hex, :hex_core, "0.10.0", "6e739a159b0141fa6c3c60c92b73aa6dec5b7909647a9b9ecea9da6709b75709", [:rebar3], [], "hexpm", "1c229aeb2df3a7ffc0c00fa4fc1721995058b2c617f083cf617e29258b1d9f57"}, + "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_diff": {:hex, :makeup_diff, "0.1.1", "01498f8c95970081297837eaf4686b6f3813e535795b8421f15ace17a59aea37", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "fadb0bf014bd328badb7be986eadbce1a29955dd51c27a9e401c3045cf24184e"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"}, - "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, + "postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, - "stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"}, + "stream_data": {:hex, :stream_data, "1.1.2", "05499eaec0443349ff877aaabc6e194e82bda6799b9ce6aaa1aadac15a9fdb4d", [:mix], [], "hexpm", "129558d2c77cbc1eb2f4747acbbea79e181a5da51108457000020a906813a1a9"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, }