├── .credo.exs ├── .dialyzer-ignore.exs ├── .formatter.exs ├── .github └── workflows │ ├── credo.yml │ ├── dialyzer.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── mix │ └── tasks │ │ └── gen.metrics.ex ├── prometheus_telemetry.ex └── prometheus_telemetry │ ├── config.ex │ ├── metrics │ ├── cowboy.ex │ ├── ecto.ex │ ├── finch.ex │ ├── graphql.ex │ ├── graphql │ │ ├── complexity.ex │ │ ├── query_name.ex │ │ └── request.ex │ ├── oban.ex │ ├── phoenix.ex │ ├── swoosh.ex │ └── vm.ex │ ├── metrics_exporter_plug.ex │ ├── periodic_measurements │ └── erlang_vm.ex │ ├── router.ex │ └── utils.ex ├── mix.exs ├── mix.lock ├── priv └── metric_template.ex.eex └── test ├── prometheus_telemetry ├── metrics │ └── ecto_test.exs ├── periodic_measurements │ └── erlang_vm_test.exs └── router_test.exs ├── prometheus_telemetry_test.exs ├── support ├── mock_supervisor.ex └── test_helpers.ex └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | allowed_imports = [ 2 | [:Plug], 3 | [:Telemetry, :Metrics] 4 | ] 5 | 6 | %{ 7 | configs: [ 8 | %{ 9 | name: "default", 10 | files: %{ 11 | included: [ 12 | "lib/", 13 | "test/" 14 | ], 15 | excluded: [~r"_build/", ~r"deps/"] 16 | }, 17 | plugins: [], 18 | requires: ["deps/blitz_credo/lib/blitz_credo/"], 19 | strict: true, 20 | parse_timeout: 10000, 21 | color: true, 22 | checks: [ 23 | 24 | # BlitzCredoChecks 25 | 26 | {BlitzCredoChecks.SetWarningsAsErrorsInTest, false}, 27 | {BlitzCredoChecks.DocsBeforeSpecs, []}, 28 | {BlitzCredoChecks.DoctestIndent, []}, 29 | {BlitzCredoChecks.NoAsyncFalse, []}, 30 | {BlitzCredoChecks.NoDSLParentheses, []}, 31 | {BlitzCredoChecks.NoIsBitstring, []}, 32 | {BlitzCredoChecks.StrictComparison, []}, 33 | {BlitzCredoChecks.UseStream, []}, 34 | {BlitzCredoChecks.LowercaseTestNames, []}, 35 | {BlitzCredoChecks.ImproperImport, allowed_modules: allowed_imports}, 36 | 37 | # Consistency Checks 38 | {Credo.Check.Consistency.ExceptionNames, []}, 39 | {Credo.Check.Consistency.LineEndings, []}, 40 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 41 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 42 | {Credo.Check.Consistency.SpaceInParentheses, []}, 43 | {Credo.Check.Consistency.TabsOrSpaces, []}, 44 | 45 | # Design Checks 46 | {Credo.Check.Design.AliasUsage, false}, 47 | 48 | # No outstanding TODOs 49 | {Credo.Check.Design.TagTODO, []}, 50 | {Credo.Check.Design.TagFIXME, []}, 51 | 52 | # # Readability Checks 53 | {Credo.Check.Readability.AliasOrder, false}, 54 | {Credo.Check.Readability.FunctionNames, []}, 55 | {Credo.Check.Readability.LargeNumbers, []}, 56 | {Credo.Check.Readability.MaxLineLength, [max_length: 120]}, 57 | {Credo.Check.Readability.ModuleAttributeNames, []}, 58 | {Credo.Check.Readability.ModuleDoc, false}, 59 | {Credo.Check.Readability.ModuleNames, []}, 60 | {Credo.Check.Readability.ParenthesesInCondition, []}, 61 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 62 | {Credo.Check.Readability.PredicateFunctionNames, []}, 63 | {Credo.Check.Readability.PreferImplicitTry, []}, 64 | {Credo.Check.Readability.RedundantBlankLines, false}, 65 | {Credo.Check.Readability.Semicolons, []}, 66 | {Credo.Check.Readability.SpaceAfterCommas, false}, 67 | {Credo.Check.Readability.StringSigils, []}, 68 | {Credo.Check.Readability.TrailingBlankLine, false}, 69 | {Credo.Check.Readability.TrailingWhiteSpace, false}, 70 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 71 | {Credo.Check.Readability.VariableNames, []}, 72 | # 73 | # Refactoring Opportunities 74 | {Credo.Check.Refactor.CondStatements, []}, 75 | {Credo.Check.Refactor.CyclomaticComplexity, false}, 76 | {Credo.Check.Refactor.FunctionArity, []}, 77 | {Credo.Check.Refactor.LongQuoteBlocks, false}, 78 | {Credo.Check.Refactor.MapInto, false}, 79 | {Credo.Check.Refactor.MatchInCondition, []}, 80 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 81 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 82 | {Credo.Check.Refactor.Nesting, false}, 83 | {Credo.Check.Refactor.UnlessWithElse, []}, 84 | {Credo.Check.Refactor.WithClauses, []}, 85 | 86 | # Warnings 87 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 88 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 89 | {Credo.Check.Warning.IExPry, []}, 90 | {Credo.Check.Warning.IoInspect, []}, 91 | {Credo.Check.Warning.LazyLogging, false}, 92 | {Credo.Check.Warning.MixEnv, false}, 93 | {Credo.Check.Warning.OperationOnSameValues, []}, 94 | {Credo.Check.Warning.OperationWithConstantResult, []}, 95 | {Credo.Check.Warning.RaiseInsideRescue, []}, 96 | {Credo.Check.Warning.UnusedEnumOperation, []}, 97 | {Credo.Check.Warning.UnusedFileOperation, []}, 98 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 99 | {Credo.Check.Warning.UnusedListOperation, []}, 100 | {Credo.Check.Warning.UnusedPathOperation, []}, 101 | {Credo.Check.Warning.UnusedRegexOperation, []}, 102 | {Credo.Check.Warning.UnusedStringOperation, []}, 103 | {Credo.Check.Warning.UnusedTupleOperation, []}, 104 | {Credo.Check.Warning.UnsafeExec, []}, 105 | 106 | # Controversial and experimental checks 107 | {Credo.Check.Readability.StrictModuleLayout, false}, 108 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 109 | {Credo.Check.Consistency.UnusedVariableNames, false}, 110 | {Credo.Check.Design.DuplicatedCode, false}, 111 | {Credo.Check.Readability.AliasAs, false}, 112 | {Credo.Check.Readability.MultiAlias, false}, 113 | {Credo.Check.Readability.Specs, false}, 114 | {Credo.Check.Readability.SinglePipe, []}, 115 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 116 | {Credo.Check.Refactor.ABCSize, false}, 117 | {Credo.Check.Refactor.AppendSingleItem, false}, 118 | {Credo.Check.Refactor.DoubleBooleanNegation, false}, 119 | {Credo.Check.Refactor.ModuleDependencies, false}, 120 | {Credo.Check.Refactor.NegatedIsNil, false}, 121 | {Credo.Check.Refactor.PipeChainStart, []}, 122 | {Credo.Check.Refactor.VariableRebinding, false}, 123 | {Credo.Check.Warning.LeakyEnvironment, false}, 124 | {Credo.Check.Warning.MapGetUnsafePass, false}, 125 | {Credo.Check.Warning.UnsafeToAtom, false} 126 | ] 127 | } 128 | ] 129 | } 130 | 131 | 132 | -------------------------------------------------------------------------------- /.dialyzer-ignore.exs: -------------------------------------------------------------------------------- 1 | [ 2 | ~r|test/support/mock_supervisor.ex:.*:.*setup\/0| 3 | ] 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | import_deps: [:plug], 4 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /.github/workflows/credo.yml: -------------------------------------------------------------------------------- 1 | name: Credo 2 | 3 | on: push 4 | 5 | jobs: 6 | Credo: 7 | runs-on: ubuntu-latest 8 | 9 | env: 10 | MIX_ENV: test 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Set up Elixir 16 | uses: erlef/setup-beam@v1 17 | with: 18 | elixir-version: '1.15.2' # Define the elixir version [required] 19 | otp-version: '26.0' # Define the OTP version [required] 20 | 21 | - name: Cache Deps & Build 22 | uses: actions/cache@v4 23 | with: 24 | key: ${{ runner.os }}-mix-credo-${{ hashFiles('**/mix.lock') }} 25 | path: | 26 | deps 27 | _build 28 | restore-keys: | 29 | ${{ runner.os }}-mix-credo- 30 | 31 | - name: Install Dependencies 32 | run: mix deps.get 33 | 34 | - name: Compile Project 35 | run: mix compile 36 | 37 | - name: Run Credo 38 | run: mix credo 39 | -------------------------------------------------------------------------------- /.github/workflows/dialyzer.yml: -------------------------------------------------------------------------------- 1 | name: Dialyzer 2 | 3 | on: push 4 | 5 | jobs: 6 | Dialyzer: 7 | runs-on: ubuntu-latest 8 | 9 | env: 10 | MIX_ENV: test 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Set up Elixir 16 | uses: erlef/setup-beam@v1 17 | with: 18 | elixir-version: '1.15.2' # Define the elixir version [required] 19 | otp-version: '26.0' # Define the OTP version [required] 20 | 21 | - name: Cache Deps & Build 22 | uses: actions/cache@v3 23 | with: 24 | key: ${{ runner.os }}-mix-dialyzer-${{ hashFiles('**/mix.lock') }} 25 | path: | 26 | deps 27 | _build 28 | .dialyzer 29 | restore-keys: | 30 | ${{ runner.os }}-mix-dialyzer- 31 | 32 | - name: Install Dependencies 33 | run: mix deps.get 34 | 35 | - name: Compile Project 36 | run: mix compile 37 | 38 | - name: Run Dialyzer 39 | run: mkdir -p .dialyzer && mix dialyzer 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: push 4 | 5 | jobs: 6 | Test: 7 | runs-on: ubuntu-latest 8 | 9 | env: 10 | MIX_ENV: test 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Set up Elixir 16 | uses: erlef/setup-beam@v1 17 | with: 18 | elixir-version: '1.15.2' # Define the elixir version [required] 19 | otp-version: '26.0' # Define the OTP version [required] 20 | 21 | - name: Cache Deps & Build 22 | uses: actions/cache@v3 23 | with: 24 | key: ${{ runner.os }}-mix-test-${{ hashFiles('**/mix.lock') }} 25 | path: | 26 | deps 27 | _build 28 | restore-keys: | 29 | ${{ runner.os }}-mix-test- 30 | 31 | - name: Install Dependencies 32 | run: mix deps.get 33 | 34 | - name: Compile Project 35 | run: mix compile 36 | 37 | - name: Run Tests 38 | run: mix test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Elixir ### 2 | /_build 3 | /cover 4 | /deps 5 | /doc 6 | /.fetch 7 | erl_crash.dump 8 | *.ez 9 | *.beam 10 | /config/*.secret.exs 11 | .elixir_ls/ 12 | .dialyzer/ 13 | 14 | ### Elixir Patch ### 15 | 16 | ### OSX ### 17 | # General 18 | .DS_Store 19 | .AppleDouble 20 | .LSOverride 21 | 22 | # Icon must end with two \r 23 | Icon 24 | 25 | # Thumbnails 26 | ._* 27 | 28 | # Files that might appear in the root of a volume 29 | .DocumentRevisions-V100 30 | .fseventsd 31 | .Spotlight-V100 32 | .TemporaryItems 33 | .Trashes 34 | .VolumeIcon.icns 35 | .com.apple.timemachine.donotpresent 36 | 37 | # Directories potentially created on remote AFP share 38 | .AppleDB 39 | .AppleDesktop 40 | Network Trash Folder 41 | Temporary Items 42 | .apdisk 43 | 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | ### v0.4.13 3 | - Remove newlines from the Ecto query string 4 | 5 | ### v0.4.12 6 | - update telemetry_metrics and telemetry_metrics_prometheus_core 7 | - allow passing `ip` option to cowboy 8 | - fix missing first char in shortened query 9 | - merge duplicated finch tags 10 | 11 | ### v0.4.11 12 | - Fix for Finch metadata extraction 13 | 14 | ### v0.4.10 15 | - Fix for waiting for app loads before compiling modules 16 | 17 | ### v0.4.8 18 | - Fix for exporter enabled typo 19 | 20 | ### v0.4.7 21 | - Swap from port checking to storing started state in persistant term 22 | 23 | ### v0.4.6 24 | - add a small delay after closing the port to ensure it can be opened again 25 | 26 | ### v0.4.5 27 | - Fix supervisor startup leading to application hangs 28 | - Add test for exporter port so we get better debugging details 29 | 30 | ### v0.4.4 31 | - Add priv folder to releases so generator works 32 | 33 | ### v0.4.3 34 | - Adds ability to generate metrics modules via `prometheus_telemetry.gen.metrics` 35 | - Fixes erlang poller metrics 36 | 37 | ### v0.4.2 38 | - Fix issue with max_idle time disconnect metrics from Finch 39 | 40 | ### v0.4.1 41 | - Fix issue that caused metrics to crash if no known query module attached 42 | 43 | ### v0.4.0 44 | - phoenix.endpoint_call.count metric for calls 45 | - clamp ecto query to 150 chars by default 46 | - add ability to set KnownQuerys module 47 | - add ability to set known query in repo call 48 | 49 | ### v0.3.2 50 | - Add response code count for statuses 51 | 52 | ### v0.3.1 53 | - Make sure we transform all keys to strings for gql metrics to avoid collisions 54 | 55 | ### v0.3.0 56 | - Swap microseconds to millseconds globally 57 | - Change default microsecond buckets 58 | 59 | ### v0.2.8 60 | - Expose method on finch metrics 61 | 62 | ### v0.2.7 63 | - Add additional swoosh info 64 | - Add additional finch metric info 65 | 66 | ### v0.2.6 67 | - Fix mailer naming for swoosh 68 | 69 | ### v0.2.5 70 | - Add a case for when regex fails to parse gql names 71 | - Relax dependency requirements 72 | 73 | ### v0.2.4 74 | - Add finch metrics 75 | - Add cowboy metrics 76 | - Add swoosh metrics 77 | 78 | ### v0.2.3 79 | - Fix oban metrics 80 | 81 | ### v0.2.2 82 | - Add oban metrics 83 | 84 | ### v0.2.1 85 | - Add vm metrics that aren't periodic 86 | - Make sure all apps are required only if lib is found 87 | - Fix graphql request_name 88 | 89 | ### v0.2.0 90 | - Add periodic measurements and add a Beam Uptime metric 91 | 92 | ### v0.1.0 93 | - Initial Release 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 theblitzapp 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PrometheusTelemetry 2 | [![Test](https://github.com/theblitzapp/prometheus_telemetry_elixir/actions/workflows/test.yml/badge.svg)](https://github.com/theblitzapp/prometheus_telemetry_elixir/actions/workflows/test.yml) 3 | [![Credo](https://github.com/theblitzapp/prometheus_telemetry_elixir/actions/workflows/credo.yml/badge.svg)](https://github.com/theblitzapp/prometheus_telemetry_elixir/actions/workflows/credo.yml) 4 | [![Dialyzer](https://github.com/theblitzapp/prometheus_telemetry_elixir/actions/workflows/dialyzer.yml/badge.svg)](https://github.com/theblitzapp/prometheus_telemetry_elixir/actions/workflows/dialyzer.yml) 5 | [![Hex version badge](https://img.shields.io/hexpm/v/prometheus_telemetry.svg)](https://hex.pm/packages/prometheus_telemetry) 6 | 7 | PrometheusTelemetry is the plumbing for Telemetry.Metrics and allows the 8 | metrics passed in to be collected and exported in the format expected 9 | by the prometheus scraper. 10 | 11 | This supervisor also contains the ability to spawn an exporter which will 12 | scrape every supervisor running for metrics and will spin up a plug and return 13 | it at `/metrics` on port 4050 by default, this will work out of the box with umbrella apps as well and allow you to define metrics in each umbrella app 14 | 15 | ### Installation 16 | 17 | The package can be installed by adding `prometheus_telemetry` to your list of dependencies in `mix.exs`: 18 | 19 | ```elixir 20 | def deps do 21 | [ 22 | {:prometheus_telemetry, "~> 0.4"} 23 | ] 24 | end 25 | ``` 26 | 27 | Documentation can be found at . 28 | 29 | 30 | ### Example 31 | 32 | We can add this to any application we want: 33 | 34 | ```elixir 35 | children = [ 36 | {PrometheusTelemetry, metrics: [ 37 | MyMetricsModule.metrics() 38 | ]} 39 | ] 40 | ``` 41 | 42 | Then to setup an exporter, on a server application like a phoenix app or pipeline 43 | we can setup the exporter which will start the metrics server (by default on port `localhost:4050`): 44 | 45 | ```elixir 46 | children = [ 47 | {PrometheusTelemetry, 48 | exporter: [enabled?: true], 49 | metrics: MyMetricsModule.metrics() 50 | } 51 | ] 52 | ``` 53 | 54 | ### Built-in Metrics 55 | There are built-in metrics for some erlang vm stats, phoenix, absinthe, ecto and oban. To enable them we can use the following modules: 56 | 57 | `PrometheusTelemetry.Metrics.{Ecto,Cowboy,Swoosh,Finch,Phoenix,GraphQL,Oban,VM}` 58 | 59 | (See [the notes below](#ecto-extras) on conveniences for configuring metrics for Ecto repos.) 60 | 61 | ```elixir 62 | children = [ 63 | {PrometheusTelemetry, 64 | exporter: [enabled?: true], 65 | metrics: [ 66 | PrometheusTelemetry.Metrics.Ecto.metrics(:my_ecto_app), 67 | PrometheusTelemetry.Metrics.Cowboy.metrics(), 68 | PrometheusTelemetry.Metrics.Swoosh.metrics(), 69 | PrometheusTelemetry.Metrics.Finch.metrics(), 70 | PrometheusTelemetry.Metrics.Phoenix.metrics(), 71 | PrometheusTelemetry.Metrics.GraphQL.metrics(), 72 | PrometheusTelemetry.Metrics.Oban.metrics(), 73 | PrometheusTelemetry.Metrics.VM.metrics() 74 | ] 75 | } 76 | ] 77 | ``` 78 | 79 | ### Default Config 80 | These are the default config settings, you can override by setting any of them 81 | 82 | ``` 83 | config :prometheus_telemetry, 84 | default_millisecond_buckets: [100, 300, 500, 1000, 2000, 5000, 10_000], 85 | default_microsecond_buckets: [50_000, 100_000, 250_000, 500_000, 750_000], 86 | measurement_poll_period: :timer.seconds(10), 87 | ecto_max_query_length: 150, 88 | ecto_known_query_module: nil 89 | ``` 90 | 91 | ### Defining Custom Metrics 92 | To define metrics we can create a module to group them like `MyMetricsModule` 93 | 94 | ```elixir 95 | defmodule MyMetricsModule do 96 | import Telemetry.Metrics, only: [last_value: 2, counter: 2] 97 | 98 | def metrics do 99 | [ 100 | counter( 101 | "prometheus_name.to_save", # becomes prometheus_name_to_save in prometheus 102 | event_name: [:event_namespace, :my_metric], # telemetry event name 103 | measurement: :count, # telemetry event metric 104 | description: "some description" 105 | ), 106 | 107 | last_value( 108 | "my_custom.name", 109 | event_name: [:event_namespace, :last_value], 110 | measurement: :total, 111 | description: "my value", 112 | tags: [:custom_metric] # custom metrics to save, derived from :telemetry.execute metadata 113 | ) 114 | ] 115 | end 116 | 117 | def inc_to_save do 118 | :telemetry.execute([:event_namespace, :my_metric], %{count: 1}) 119 | end 120 | 121 | def set_custom_name do 122 | :telemetry.execute([:event_namespace, :last_value], %{total: 123}, %{custom_metric: "region"}) 123 | end 124 | end 125 | ``` 126 | 127 | Ultimately every list will get flattened which allows you to group metric modules under a single module such as 128 | 129 | ```elixir 130 | defmodule GraphQL.Request do 131 | def metrics do 132 | ... 133 | end 134 | end 135 | 136 | defmodule GraphQL.Complexity do 137 | def metrics do 138 | ... 139 | end 140 | end 141 | 142 | defmodule GraphQL do 143 | def metrics, do: [GraphQL.Complexity.metrics(), GraphQL.Request.metrics()] 144 | end 145 | ``` 146 | 147 | For more details on types you can check [telemetry_metrics_prometheus_core](https://hexdocs.pm/telemetry_metrics_prometheus_core/1.0.1/TelemetryMetricsPrometheus.Core.html) 148 | You can also generate these custom metrics modules using `mix prometheus_telemetry.gen.metrics` 149 | 150 | #### Ecto Extras 151 | This library hooks into [Ecto's adapter-specific telemetry events](https://hexdocs.pm/ecto/Ecto.Repo.html#module-adapter-specific-events). In order to correctly namespace events for your repo(s), you must pass an atom (or list of atoms) when injecting the metrics. E.g.: `PrometheusTelemetry.Metrics.Ecto.metrics(:my_ecto_app)` will produce corresponding metrics with the correct event_name of `[:my_ecto_app, :repo, :query]`. 152 | 153 | `PrometheusTelemetry.Metrics.Ecto.metrics_for_repo/1` and `PrometheusTelemetry.Metrics.Ecto.metrics_for_repos/1` can be used as a convenience for umbrellas with multiple repos and for deduplicating metrics 154 | for replicas: 155 | 156 | ```elixir 157 | iex> PrometheusTelemetry.Metrics.Ecto.metrics_for_repo(MyEctoApp.Repo) 158 | [ 159 | %Telemetry.Metrics.Distribution{ 160 | description: "Gets total time spent on query", 161 | event_name: [:my_ecto_app, :repo, :query], 162 | ... 163 | }, 164 | ... 165 | ] 166 | 167 | ``` 168 | 169 | A few additional extras exist for Ecto, which include the ability to set the max query size before truncation 170 | as well as add a known module query which will be called with `shorten(query)` 171 | 172 | You can set both of these via config, by default there's no known query module and the max query size is 150. 173 | 174 | The other way to set the label for a query is directly in the `Repo.all` or `Actions.all` (if using [ecto_shorts](https://github.com/MikaAK/ecto_shorts)) 175 | 176 | Repo: 177 | ```elixir 178 | Repo.all(User, telemetry_options: [label: "All Users"]) 179 | ``` 180 | 181 | EctoShorts: 182 | ```elixir 183 | Actions.find(User, params, telemetry_options: [label: "Find User"]) 184 | ``` 185 | 186 | #### Generating Metrics Modules 187 | Using the `mix prometheus_telemetry.gen.metrics` you can generate metrics modules instead of writing them manually 188 | 189 | ```bash 190 | $ mix prometheus_telemetry.gen.metrics MyApp.Metrics.Type counter:event.name.measurement.count:event.name.count:count:tags:profile:region 191 | ``` 192 | 193 | The format used is the following: 194 | ```elixir 195 | ::::tags:: 196 | ``` 197 | 198 | View the `Mix.Tasks.PrometheusTelemetry.Gen.Metrics` module for more details 199 | 200 | ### Hiring 201 | 202 | Are you looking for a new gig?? We're looking for mid-level to senior level developers to join our team and continue growing our platform while building awesome software! 203 | 204 | Come join us at [Blitz.gg](https://blitz.gg/careers) 205 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :prometheus_telemetry, 4 | default_microsecond_buckets: [], 5 | default_millisecond_buckets: [] 6 | -------------------------------------------------------------------------------- /lib/mix/tasks/gen.metrics.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.PrometheusTelemetry.Gen.Metrics do 2 | @shortdoc "Generates Metrics modules" 3 | @moduledoc """ 4 | This can be used to generate metrics modules 5 | 6 | The format used is 7 | ```elixir 8 | ::::tags:: 9 | ``` 10 | 11 | ### Example 12 | ```bash 13 | $ mix prometheus_telemetry.gen.metrics MyApp.Metrics.Type counter:event.name.measurement.count:event.name.count:count:tags:profile:region 14 | ``` 15 | 16 | The following metrics have been implemented: 17 | `counter`, `distribution`, `last_value` and `sum` 18 | 19 | With `distribution` you can also provide a second parameter of milliseconds, seconds or microseconds 20 | """ 21 | 22 | use Mix.Task 23 | 24 | def run([]) do 25 | Mix.raise("Must supply arguments to prometheus_telemetry.gen.metrics") 26 | end 27 | 28 | def run([metrics_module | measurements]) do 29 | 30 | measurements 31 | |> Enum.map(&(&1 |> String.split(":") |> parse_measurements)) 32 | |> build_metrics_module_from_measurements(metrics_module) 33 | |> write_metrics_file(metrics_module) 34 | end 35 | 36 | defp parse_measurements(["counter", metric_name, event_name, measurement | tags]) do 37 | [ 38 | type: :counter, 39 | metric_name: metric_name, 40 | event_name: parse_event_name(event_name), 41 | event_function: parse_function_name(event_name, measurement), 42 | measurement: String.to_atom(measurement), 43 | tags: parse_tags(tags) 44 | ] 45 | end 46 | 47 | defp parse_measurements(["distribution", "milliseconds", metric_name, event_name, measurement | tags]) do 48 | [ 49 | type: :distribution, 50 | metric_name: metric_name, 51 | event_name: parse_event_name(event_name), 52 | event_function: parse_function_name(event_name, measurement), 53 | measurement: String.to_atom(measurement), 54 | unit: :milliseconds, tags: parse_tags(tags) 55 | ] 56 | end 57 | 58 | defp parse_measurements(["distribution", "microseconds", metric_name, event_name, measurement | tags]) do 59 | [ 60 | type: :distribution, 61 | metric_name: metric_name, 62 | event_name: parse_event_name(event_name), 63 | event_function: parse_function_name(event_name, measurement), 64 | measurement: String.to_atom(measurement), 65 | unit: :microseconds, tags: parse_tags(tags) 66 | ] 67 | end 68 | 69 | defp parse_measurements(["distribution", "seconds", metric_name, event_name, measurement | tags]) do 70 | [ 71 | type: :distribution, 72 | metric_name: metric_name, 73 | event_name: parse_event_name(event_name), 74 | event_function: parse_function_name(event_name, measurement), 75 | measurement: String.to_atom(measurement), 76 | tags: parse_tags(tags) 77 | ] 78 | end 79 | 80 | defp parse_measurements(["distribution", metric_name, event_name, measurement | tags]) do 81 | [ 82 | type: :distribution, 83 | metric_name: metric_name, 84 | event_name: parse_event_name(event_name), 85 | event_function: parse_function_name(event_name, measurement), 86 | measurement: String.to_atom(measurement), 87 | unit: :milliseconds, tags: parse_tags(tags) 88 | ] 89 | end 90 | 91 | defp parse_measurements(["last_value", metric_name, event_name, measurement | tags]) do 92 | [ 93 | type: :last_value, 94 | metric_name: metric_name, 95 | event_name: parse_event_name(event_name), 96 | event_function: parse_function_name(event_name, measurement), 97 | measurement: String.to_atom(measurement), 98 | tags: parse_tags(tags) 99 | ] 100 | end 101 | 102 | defp parse_measurements(["sum", metric_name, event_name, measurement | tags]) do 103 | [ 104 | type: :sum, 105 | metric_name: metric_name, 106 | event_name: parse_event_name(event_name), 107 | event_function: parse_function_name(event_name, measurement), 108 | measurement: String.to_atom(measurement), 109 | tags: parse_tags(tags) 110 | ] 111 | end 112 | 113 | defp parse_tags(["tags" | tags]) when tags !== [] do 114 | Enum.map(tags, &String.to_atom/1) 115 | end 116 | 117 | defp parse_tags(_) do 118 | [] 119 | end 120 | 121 | defp parse_event_name(event_name) do 122 | event_name |> String.split(".") |> Enum.map(&String.to_atom/1) 123 | end 124 | 125 | defp parse_function_name(event_name, measurement) do 126 | event_name 127 | |> String.replace(".", "_") 128 | |> String.trim_trailing("_#{measurement}") 129 | |> String.trim_trailing(measurement) 130 | end 131 | 132 | defp build_metrics_module_from_measurements(measurements, metrics_module) do 133 | :prometheus_telemetry 134 | |> :code.priv_dir 135 | |> Path.join("metric_template.ex.eex") 136 | |> EEx.eval_file( 137 | assigns: %{ 138 | metrics: measurements, 139 | module_name: metrics_module, 140 | metrics_imports: measurement_metrics_imports(measurements) 141 | } 142 | ) 143 | |> Code.format_string! 144 | end 145 | 146 | defp measurement_metrics_imports(measurements) do 147 | measurements 148 | |> Enum.group_by(&(&1[:type])) 149 | |> Map.keys 150 | |> Enum.map(fn type -> {type, 2} end) 151 | end 152 | 153 | defp write_metrics_file(metrics_file_contents, metrics_module) do 154 | file_path = module_file_path(metrics_module) 155 | 156 | Mix.Generator.create_file(file_path, metrics_file_contents) 157 | end 158 | 159 | defp module_file_path(metrics_module) do 160 | metrics_path = metrics_module 161 | |> String.split(".") 162 | |> Enum.map(&Macro.underscore/1) 163 | |> then(&List.update_at(&1, length(&1) - 1, fn file_name -> "#{file_name}.ex" end)) 164 | 165 | Path.join(["lib" | metrics_path]) 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry do 2 | @definition [ 3 | name: [ 4 | type: :atom, 5 | default: :prometheus_telemetry, 6 | doc: "Name for the prometheus telemetry supervisor" 7 | ], 8 | 9 | exporter: [ 10 | type: :keyword_list, 11 | default: [enabled?: false, opts: []], 12 | doc: "Exporter config", 13 | keys: [ 14 | enabled?: [ 15 | type: :boolean, 16 | default: false 17 | ], 18 | 19 | opts: [ 20 | type: :keyword_list, 21 | default: [], 22 | doc: "Exporter options", 23 | keys: [ 24 | ip: [ 25 | type: {:tuple, [:non_neg_integer, :non_neg_integer, :non_neg_integer, :non_neg_integer]}, 26 | default: {0, 0, 0, 0}, 27 | doc: "IP address to bind the exporter on", 28 | ], 29 | 30 | port: [ 31 | type: :integer, 32 | default: 4050, 33 | doc: "Port to start the exporter on" 34 | ], 35 | 36 | protocol: [ 37 | type: {:in, [:http, :https]}, 38 | default: :http 39 | ] 40 | ] 41 | ] 42 | ] 43 | ], 44 | 45 | metrics: [ 46 | type: {:list, :any}, 47 | doc: "Metrics to start and aggregate that will ultimately end up in the exporter" 48 | ], 49 | 50 | periodic_measurements: [ 51 | type: {:list, :any}, 52 | doc: "Periodic metrics to start and aggregate that will ultimately end up in the exporter" 53 | ] 54 | ] 55 | 56 | @external_resource "./README.md" 57 | 58 | @moduledoc """ 59 | #{File.read!("./README.md")}" 60 | 61 | ### Supported Options 62 | #{NimbleOptions.docs(@definition)} 63 | """ 64 | 65 | require Logger 66 | 67 | use Supervisor 68 | 69 | alias PrometheusTelemetry 70 | 71 | @poller_postfix "poller" 72 | @supervisor_postfix "prometheus_telemetry_supervisor" 73 | @watcher_postfix "metrics_watcher" 74 | @exporter_enabled_key :__exporter_ports__ 75 | 76 | def get_metrics_string(name) do 77 | get_supervisors_metrics_string([name]) 78 | end 79 | 80 | def get_metrics_string do 81 | get_supervisors_metrics_string(list()) 82 | end 83 | 84 | defp get_supervisors_metrics_string(supervisors) do 85 | supervisors 86 | |> Stream.flat_map(&list_prometheus_cores/1) 87 | |> Enum.map_join("\n", &TelemetryMetricsPrometheus.Core.scrape/1) 88 | end 89 | 90 | def list do 91 | # This could be better optimized via a registry 92 | Enum.filter(Process.registered(), &String.ends_with?(to_string(&1), @supervisor_postfix)) 93 | end 94 | 95 | def list_prometheus_cores(supervisor) do 96 | supervisor 97 | |> Supervisor.which_children() 98 | |> Enum.reduce([], fn child, acc -> 99 | case prometheus_core_name(child) do 100 | nil -> acc 101 | metrics_core_name when is_list(metrics_core_name) -> metrics_core_name ++ acc 102 | metrics_core_name -> [metrics_core_name | acc] 103 | end 104 | end) 105 | end 106 | 107 | def poller_postfix, do: @poller_postfix 108 | 109 | defp prometheus_core_name( 110 | {metrics_core_name, _, _, [TelemetryMetricsPrometheus.Core.Registry]} 111 | ), 112 | do: metrics_core_name 113 | 114 | defp prometheus_core_name(_), do: nil 115 | 116 | @spec start_link(Keyword.t) :: {:ok, pid} | :ignore | {:error, {:shutdown, term()} | term()} 117 | def start_link(opts \\ []) do 118 | opts = NimbleOptions.validate!(opts, @definition) 119 | exporter_config = opts[:exporter] 120 | 121 | exporter_enabled? = if exporter_config[:enabled?] and 122 | not exporter_already_enabled?(exporter_config[:opts][:port]) do 123 | put_exporter_enabled(exporter_config[:opts][:port]) 124 | 125 | true 126 | else 127 | false 128 | end 129 | 130 | params = %{ 131 | name: :"#{opts[:name]}_#{Enum.random(1..100_000_000_000)}", 132 | enable_exporter?: exporter_enabled?, 133 | exporter_opts: exporter_config[:opts], 134 | metrics: opts[:metrics], 135 | pollers: opts[:periodic_measurements] 136 | } 137 | 138 | original_name = opts[:name] 139 | opts = Keyword.update!(opts, :name, &:"#{&1}_#{@supervisor_postfix}") 140 | 141 | if is_nil(params.pollers) and is_nil(params.metrics) and not params.enable_exporter? do 142 | raise "Must provide at least one of opts[:pollers] or opts[:metrics] to PrometheusTelemetry or enable the exporter" 143 | end 144 | 145 | with {:error, {:already_started, _}} <- Supervisor.start_link(PrometheusTelemetry, params, opts) do 146 | opts = Keyword.put(opts, :name, :"#{original_name}_#{Enum.random(1..100_000_000_000)}_#{@supervisor_postfix}") 147 | 148 | Supervisor.start_link(PrometheusTelemetry, params, opts) 149 | end 150 | end 151 | 152 | defp exporter_already_enabled?(port), do: port in :persistent_term.get(@exporter_enabled_key, []) 153 | 154 | defp put_exporter_enabled(port) do 155 | :persistent_term.put(@exporter_enabled_key, [port | :persistent_term.get(@exporter_enabled_key, [])]) 156 | end 157 | 158 | @impl true 159 | def init( 160 | %{ 161 | name: name, 162 | pollers: pollers, 163 | metrics: metrics 164 | } = params 165 | ) do 166 | children = maybe_create_children(name, metrics, pollers) ++ maybe_create_exporter_child(params) 167 | 168 | Supervisor.init(children, strategy: :one_for_one) 169 | end 170 | 171 | defp maybe_create_children(name, metrics, pollers) do 172 | maybe_create_poller_child(name, pollers) ++ maybe_create_metrics_child(name, metrics) 173 | end 174 | 175 | defp maybe_create_exporter_child(%{ 176 | enable_exporter?: true, 177 | exporter_opts: opts 178 | }), do: create_exporter_child(opts) 179 | 180 | defp maybe_create_exporter_child(_), do: [] 181 | 182 | defp create_exporter_child(opts) do 183 | # need to do a check if exporter child is up 184 | [PrometheusTelemetry.MetricsExporterPlug.child_spec(opts)] 185 | end 186 | 187 | defp maybe_create_metrics_child(name, metrics) when is_list(metrics) do 188 | [ 189 | {TelemetryMetricsPrometheus.Core, 190 | metrics: List.flatten(metrics), name: :"#{name}_#{@watcher_postfix}"} 191 | ] 192 | end 193 | 194 | defp maybe_create_metrics_child(_, _) do 195 | [] 196 | end 197 | 198 | defp maybe_create_poller_child(name, [_ | _] = pollers) do 199 | [ 200 | { 201 | :telemetry_poller, 202 | measurements: List.flatten(pollers), 203 | period: PrometheusTelemetry.Config.measurement_poll_period(), 204 | name: :"#{name}_#{@poller_postfix}" 205 | } 206 | ] 207 | end 208 | 209 | defp maybe_create_poller_child(_, _), do: [] 210 | end 211 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/config.ex: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.Config do 2 | @moduledoc false 3 | 4 | @app :prometheus_telemetry 5 | 6 | @default_microsecond_buckets [50_000, 100_000, 250_000, 500_000, 750_000] 7 | @default_millisecond_buckets [100, 300, 500, 1000, 2000, 5000, 10_000] 8 | @default_poll_period :timer.seconds(10) 9 | @default_ecto_max_query_length 150 10 | 11 | 12 | def default_microsecond_buckets do 13 | Application.get_env(@app, :default_microsecond_buckets) || @default_microsecond_buckets 14 | end 15 | 16 | def default_millisecond_buckets do 17 | Application.get_env(@app, :default_millisecond_buckets) || @default_millisecond_buckets 18 | end 19 | 20 | def measurement_poll_period do 21 | Application.get_env(@app, :measurement_poll_period) || @default_poll_period 22 | end 23 | 24 | def ecto_known_query_module do 25 | Application.get_env(@app, :ecto_known_query_module) 26 | end 27 | 28 | def ecto_max_query_length do 29 | Application.get_env(@app, :ecto_max_query_length) || @default_ecto_max_query_length 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics/cowboy.ex: -------------------------------------------------------------------------------- 1 | if PrometheusTelemetry.Utils.app_loaded?(:cowboy) do 2 | defmodule PrometheusTelemetry.Metrics.Cowboy do 3 | @moduledoc """ 4 | These metrics give you data around Cowboy, the low level web handler of phoenix 5 | 6 | - `cowboy.request.count` 7 | - `cowboy.request.early_error.count` 8 | - `cowboy.request.exception.count` 9 | - `cowboy.request.duration.milliseconds` 10 | """ 11 | 12 | import Telemetry.Metrics, only: [distribution: 2, counter: 2] 13 | 14 | @duration_unit {:native, :millisecond} 15 | @buckets PrometheusTelemetry.Config.default_millisecond_buckets() 16 | 17 | def metrics do 18 | [ 19 | counter( 20 | "cowboy.request.count", 21 | event_name: [:cowboy, :request, :start], 22 | measurement: :count, 23 | description: "Request count for cowboy" 24 | ), 25 | 26 | counter( 27 | "cowboy.request.early_error.count", 28 | event_name: [:cowboy, :request, :early_error], 29 | measurement: :count, 30 | description: "Request count for cowboy early errors" 31 | ), 32 | 33 | counter( 34 | "cowboy.request.exception.count", 35 | event_name: [:cowboy, :request, :exception], 36 | measurement: :count, 37 | tags: [:exit_code], 38 | tag_values: &exit_code_from_metadata/1, 39 | description: "Request count for cowboy exceptions" 40 | ), 41 | 42 | distribution( 43 | "cowboy.request.duration.milliseconds", 44 | event_name: [:cowboy, :request, :stop], 45 | measurement: :duration, 46 | unit: @duration_unit, 47 | reporter_options: [buckets: @buckets], 48 | description: "Request duration for cowboy" 49 | ), 50 | ] 51 | end 52 | 53 | defp exit_code_from_metadata(opts) do 54 | %{exit_code: opts[:kind] || "Unknown"} 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics/ecto.ex: -------------------------------------------------------------------------------- 1 | if PrometheusTelemetry.Utils.app_loaded?(:ecto) do 2 | defmodule PrometheusTelemetry.Metrics.Ecto do 3 | @moduledoc """ 4 | These metrics give you metrics around phoenix requests 5 | 6 | - `ecto.query.total_time` 7 | - `ecto.query.decode_time` 8 | - `ecto.query.query_time` 9 | - `ecto.query.idle_time` 10 | """ 11 | 12 | import Telemetry.Metrics, only: [distribution: 2] 13 | 14 | @millisecond_unit {:native, :millisecond} 15 | @millisecond_buckets PrometheusTelemetry.Config.default_millisecond_buckets() 16 | @max_query_length PrometheusTelemetry.Config.ecto_max_query_length() 17 | 18 | @replica_regex "(r|R)eplica" 19 | 20 | def metrics_for_repos(repo_list) when is_list(repo_list) do 21 | repo_list 22 | |> remove_duplicate_replicas 23 | |> change_pg_module_to_string 24 | |> Enum.flat_map(&metrics/1) 25 | end 26 | 27 | def metrics_for_repo(repo) do 28 | repo 29 | |> change_pg_module_to_string 30 | |> metrics 31 | end 32 | 33 | def metrics(prefixes) when is_list(prefixes) do 34 | Enum.flat_map(prefixes, &metrics/1) 35 | end 36 | 37 | def metrics(prefix) do 38 | if is_atom(prefix) and Code.ensure_loaded?(prefix) do 39 | raise ArgumentError, """ 40 | expects an atom or a string prefix. To configure Ecto metrics using \ 41 | a module, use PrometheusTelemetry.Metrics.Ecto.metrics_for_repo/1 or \ 42 | PrometheusTelemetry.Metrics.Ecto.metrics_for_repos/1. 43 | """ 44 | end 45 | 46 | event_name = create_event_name(prefix) 47 | 48 | [ 49 | distribution( 50 | "ecto.query.total_time.milliseconds", 51 | event_name: event_name, 52 | measurement: :total_time, 53 | description: "Gets total time spent on query", 54 | unit: @millisecond_unit, 55 | reporter_options: [buckets: @millisecond_buckets], 56 | tags: [:repo, :query, :source, :result], 57 | tag_values: &format_proper_tag_values/1 58 | ), 59 | 60 | distribution( 61 | "ecto.query.decode_time.milliseconds", 62 | event_name: event_name, 63 | measurement: :decode_time, 64 | description: "Total time spent decoding query", 65 | unit: @millisecond_unit, 66 | reporter_options: [buckets: @millisecond_buckets], 67 | tags: [:repo, :query, :source, :result], 68 | tag_values: &format_proper_tag_values/1 69 | ), 70 | 71 | distribution( 72 | "ecto.query.queue_time.milliseconds", 73 | event_name: event_name, 74 | measurement: :queue_time, 75 | description: "Time spent waiting for an available connection from the pool", 76 | unit: @millisecond_unit, 77 | reporter_options: [buckets: @millisecond_buckets], 78 | tags: [:repo, :query, :source, :result], 79 | tag_values: &format_proper_tag_values/1 80 | ), 81 | 82 | distribution( 83 | "ecto.query.query_time.milliseconds", 84 | event_name: event_name, 85 | measurement: :query_time, 86 | description: "Total time spent querying", 87 | unit: @millisecond_unit, 88 | reporter_options: [buckets: @millisecond_buckets], 89 | tags: [:repo, :query, :source, :result], 90 | tag_values: &format_proper_tag_values/1 91 | ), 92 | 93 | distribution( 94 | "ecto.query.idle_time.milliseconds", 95 | event_name: event_name, 96 | measurement: :idle_time, 97 | description: "Total time spent idling", 98 | unit: @millisecond_unit, 99 | reporter_options: [buckets: @millisecond_buckets], 100 | tags: [:repo, :query, :source, :result], 101 | tag_values: &format_proper_tag_values/1 102 | ) 103 | ] 104 | end 105 | 106 | defp remove_duplicate_replicas(repo_list) do 107 | Enum.reduce(repo_list, [], fn repo, acc -> 108 | if inspect(repo) =~ ~r/#{@replica_regex}/ and replica_version_exists?(acc, repo) do 109 | acc 110 | else 111 | [repo | acc] 112 | end 113 | end) 114 | end 115 | 116 | defp replica_version_exists?(repo_list, repo) do 117 | replica_root_repo = 118 | repo 119 | |> Module.split() 120 | |> Enum.drop(-1) 121 | |> Enum.join(".") 122 | 123 | Enum.any?(repo_list, &(inspect(&1) =~ ~r/#{replica_root_repo}\.#{@replica_regex}/)) 124 | end 125 | 126 | defp create_event_name(prefix) when is_atom(prefix) do 127 | [prefix, :query] 128 | end 129 | 130 | defp create_event_name(repo_string) do 131 | repo_string 132 | |> String.split(".") 133 | |> Enum.map(fn prefix -> String.to_atom(prefix) end) 134 | |> Kernel.++([:query]) 135 | end 136 | 137 | defp change_pg_module_to_string(repos) when is_list(repos) do 138 | Enum.map(repos, &change_pg_module_to_string/1) 139 | end 140 | 141 | defp change_pg_module_to_string(repo) do 142 | repo 143 | |> inspect 144 | |> String.split(".") 145 | |> Enum.map_join(".", &Macro.underscore/1) 146 | end 147 | 148 | defp format_proper_tag_values(%{result: _result} = metadata) do 149 | {result_status, _} = metadata[:result] 150 | 151 | query = 152 | case Keyword.get(metadata[:options], :label) do 153 | nil -> 154 | maybe_shorten_query(metadata) 155 | 156 | label -> 157 | label 158 | end 159 | 160 | # Newlines are not allowed in Prometheus labels 161 | query = query |> String.replace("\n", " ") |> String.trim() 162 | 163 | metadata 164 | |> Map.update!(:repo, &inspect/1) 165 | |> Map.merge(%{ 166 | result: to_string(result_status), 167 | query: clamp_query_size(query) 168 | }) 169 | end 170 | 171 | defp clamp_query_size(query) do 172 | if String.length(query) > @max_query_length do 173 | "#{String.slice(query, 0, @max_query_length)}..." 174 | else 175 | query 176 | end 177 | end 178 | 179 | @spec maybe_shorten_query(map) :: String.t() 180 | defp maybe_shorten_query(%{query: original_query} = _metadata) do 181 | known_query_module = PrometheusTelemetry.Config.ecto_known_query_module() 182 | 183 | if known_query_module && function_exported?(known_query_module, :shorten, 1) do 184 | case known_query_module.shorten(original_query) do 185 | {:ok, shortened_query} -> shortened_query 186 | {:error, _} -> original_query 187 | end 188 | else 189 | original_query 190 | end 191 | end 192 | 193 | defp maybe_shorten_query(metadata), do: metadata 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics/finch.ex: -------------------------------------------------------------------------------- 1 | if PrometheusTelemetry.Utils.app_loaded?(:finch) do 2 | defmodule PrometheusTelemetry.Metrics.Finch do 3 | import Telemetry.Metrics, only: [counter: 2, distribution: 2] 4 | 5 | @duration_unit {:native, :millisecond} 6 | @buckets PrometheusTelemetry.Config.default_millisecond_buckets() 7 | 8 | def metrics do 9 | request_metrics() ++ pool_metrics() ++ send_receive_metrics() ++ max_idle_time_metrics() 10 | end 11 | 12 | def request_metrics do 13 | [ 14 | counter("finch.request_start.count", 15 | event_name: [:finch, :request, :start], 16 | measurement: :count, 17 | tags: [:name, :host, :port, :method], 18 | tag_values: &add_extra_metadata/1, 19 | description: "Finch request count" 20 | ), 21 | 22 | counter("finch.request_end.count", 23 | event_name: [:finch, :request, :stop], 24 | measurement: :count, 25 | tags: [:name, :host, :status, :port, :method], 26 | tag_values: fn metadata -> 27 | metadata 28 | |> add_extra_metadata 29 | |> add_status_metadata 30 | end, 31 | description: "Finch request count" 32 | ), 33 | 34 | distribution("finch.request.duration.milliseconds", 35 | event_name: [:finch, :request, :stop], 36 | measurement: :duration, 37 | description: "Finch request durations", 38 | tags: [:name, :host, :port, :method], 39 | tag_values: &add_extra_metadata/1, 40 | unit: @duration_unit, 41 | reporter_options: [buckets: @buckets] 42 | ), 43 | 44 | counter("finch.request_error.count", 45 | event_name: [:finch, :request, :exception], 46 | tags: [:name, :host, :port, :method, :reason, :kind], 47 | tag_values: &add_extra_metadata/1, 48 | measurement: :count, 49 | description: "Finch request error count" 50 | ), 51 | 52 | distribution("finch.request_error.duration.milliseconds", 53 | event_name: [:finch, :request, :exception], 54 | measurement: :duration, 55 | tags: [:reason, :kind, :host, :port, :method], 56 | tag_values: &add_extra_metadata/1, 57 | description: "Finch request error durations", 58 | unit: @duration_unit, 59 | reporter_options: [buckets: @buckets] 60 | ) 61 | ] 62 | end 63 | 64 | def pool_metrics do 65 | [ 66 | counter("finch.pool.request_connection.count", 67 | event_name: [:finch, :queue, :start], 68 | tags: [:host, :port, :method], 69 | tag_values: &add_extra_metadata/1, 70 | measurement: :count, 71 | description: "Finch count for attempting to checkout a connection from a pool" 72 | ), 73 | 74 | counter("finch.pool.checked_out_connection.count", 75 | event_name: [:finch, :queue, :stop], 76 | measurement: :count, 77 | tags: [:host, :port, :method], 78 | tag_values: &add_extra_metadata/1, 79 | description: "Finch count for attempting to checkout a connection from a pool" 80 | ), 81 | 82 | counter("finch.pool.error", 83 | event_name: [:finch, :queue, :exception], 84 | measurement: :count, 85 | tags: [:reason, :kind, :host, :port, :method], 86 | tag_values: &add_extra_metadata/1, 87 | description: "Finch count for pool errors" 88 | ), 89 | 90 | distribution("finch.pool.checked_out_connection.idle_time.milliseconds", 91 | event_name: [:finch, :queue, :stop], 92 | measurement: :idle_time, 93 | tags: [:host, :port, :method], 94 | tag_values: &add_extra_metadata/1, 95 | description: "Finch idle_time for since connection last initialized", 96 | unit: @duration_unit, 97 | reporter_options: [buckets: @buckets] 98 | ), 99 | 100 | distribution("finch.pool.checked_out_connection.duration.milliseconds", 101 | event_name: [:finch, :queue, :stop], 102 | measurement: :duration, 103 | tags: [:host, :port, :method], 104 | tag_values: &add_extra_metadata/1, 105 | description: "Finch duration for checking out a connection from pool", 106 | unit: @duration_unit, 107 | reporter_options: [buckets: @buckets] 108 | ), 109 | 110 | distribution("finch.pool.error.duration.milliseconds", 111 | event_name: [:finch, :queue, :exception], 112 | measurement: :duration, 113 | description: "Finch duration for before error occured", 114 | tags: [:reason, :kind, :host, :port, :method], 115 | tag_values: &add_extra_metadata/1, 116 | unit: @duration_unit, 117 | reporter_options: [buckets: @buckets] 118 | ) 119 | ] 120 | end 121 | 122 | def send_receive_metrics do 123 | [ 124 | counter("finch.request_send.count", 125 | event_name: [:finch, :send, :start], 126 | measurement: :count, 127 | tags: [:host, :port, :method], 128 | tag_values: &add_extra_metadata/1, 129 | measurement: :count, 130 | description: "Finch count for requests started" 131 | ), 132 | 133 | counter("finch.request_receive.count", 134 | event_name: [:finch, :recv, :start], 135 | tags: [:host, :port, :method], 136 | measurement: :count, 137 | tag_values: &add_extra_metadata/1, 138 | description: "Finch count for response receive starting" 139 | ), 140 | 141 | distribution("finch.send_end.duration.milliseconds", 142 | event_name: [:finch, :send, :stop], 143 | measurement: :duration, 144 | description: "Finch duration for how long request took to send", 145 | tags: [:error, :host, :port, :method], 146 | tag_values: fn metadata -> 147 | metadata 148 | |> add_extra_metadata 149 | |> add_error_metadata 150 | end, 151 | unit: @duration_unit, 152 | reporter_options: [buckets: @buckets] 153 | ), 154 | 155 | distribution("finch.request_end.duration.milliseconds", 156 | event_name: [:finch, :recv, :stop], 157 | measurement: :duration, 158 | description: "Finch duration for how long receiving the request took", 159 | tags: [:status, :error, :host, :port, :method], 160 | tag_values: fn metadata -> 161 | metadata 162 | |> add_extra_metadata 163 | |> add_error_metadata 164 | end, 165 | unit: @duration_unit, 166 | reporter_options: [buckets: @buckets] 167 | ) 168 | ] 169 | end 170 | 171 | defp max_idle_time_metrics do 172 | [ 173 | counter("finch.conn_max_idle_time_exceeded.count", 174 | event_name: [:finch, :conn_max_idle_time_exceeded], 175 | measurement: :count, 176 | tags: [:host, :port, :method], 177 | tag_values: &add_max_idle_time_metadata/1, 178 | description: "Finch count for Conn Max Idle Time Exceeded errors" 179 | ), 180 | 181 | counter("finch.pool_max_idle_time_exceeded.count", 182 | event_name: [:finch, :pool_max_idle_time_exceeded], 183 | measurement: :count, 184 | tags: [:host, :port, :method], 185 | tag_values: &add_max_idle_time_metadata/1, 186 | description: "Finch count for Pool Max Idle Time Exceeded errors" 187 | ) 188 | ] 189 | end 190 | 191 | defp add_max_idle_time_metadata(%{request: %Finch.Request{host: host, port: port, method: method}} = metadata) do 192 | Map.merge(metadata, %{host: host, port: port, method: method}) 193 | end 194 | 195 | defp add_max_idle_time_metadata(%{host: host, port: port, scheme: scheme}) do 196 | %{host: host, port: port, method: scheme} 197 | end 198 | 199 | defp add_max_idle_time_metadata(metadata) do 200 | metadata 201 | end 202 | 203 | defp add_error_metadata(%{error: _} = metadata), do: metadata 204 | defp add_error_metadata(metadata), do: Map.put(metadata, :error, "") 205 | 206 | defp add_extra_metadata(%{request: %Finch.Request{host: host, port: port, method: method}} = metadata) do 207 | Map.merge(metadata, %{host: host, port: port, method: method}) 208 | end 209 | 210 | defp add_extra_metadata(%{host: _, port: _, scheme: _} = metadata) do 211 | Map.put(metadata, :method, "GET") 212 | end 213 | 214 | defp add_status_metadata(%{result: {:error, _}} = metadata) do 215 | Map.put(metadata, :status, 500) 216 | end 217 | 218 | defp add_status_metadata(%{result: {:ok, %Finch.Response{status: status}}} = metadata) do 219 | Map.put(metadata, :status, status) 220 | end 221 | 222 | defp add_status_metadata(metadata) do 223 | Map.put(metadata, :status, "Unknown") 224 | end 225 | end 226 | end 227 | 228 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics/graphql.ex: -------------------------------------------------------------------------------- 1 | if PrometheusTelemetry.Utils.app_loaded?(:absinthe) do 2 | defmodule PrometheusTelemetry.Metrics.GraphQL do 3 | @moduledoc """ 4 | These metrics give you metrics around Absinthes GraphQL requests 5 | 6 | - `graphql.request.count` 7 | - `graphql.execute.duration.milliseconds` 8 | - `graphql.subscription.duration.milliseconds` 9 | - `graphql.resolve.duration.milliseconds` 10 | - `graphql.middleware.batch.duration.milliseconds` 11 | """ 12 | 13 | alias PrometheusTelemetry.Metrics.GraphQL.{Complexity, Request} 14 | 15 | def metrics do 16 | [ 17 | Request.metrics(), 18 | Complexity.metrics() 19 | ] 20 | end 21 | 22 | defdelegate observe_complexity(query, score), to: Complexity 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics/graphql/complexity.ex: -------------------------------------------------------------------------------- 1 | if PrometheusTelemetry.Utils.app_loaded?(:absinthe) do 2 | defmodule PrometheusTelemetry.Metrics.GraphQL.Complexity do 3 | @moduledoc false 4 | 5 | import Telemetry.Metrics, only: [distribution: 2] 6 | 7 | @event_prefix [:graphql] 8 | @complexity_name @event_prefix ++ [:complexity, :score] 9 | @buckets PrometheusTelemetry.Config.default_millisecond_buckets() 10 | 11 | def metrics do 12 | [ 13 | distribution( 14 | "graphql.complexity.score", 15 | event_name: @complexity_name, 16 | measurement: :score, 17 | unit: {:native, :millisecond}, 18 | description: "Gets the root complexity score for a GraphQL query", 19 | reporter_options: [buckets: @buckets], 20 | tags: [:query] 21 | ) 22 | ] 23 | end 24 | 25 | def observe_complexity(query, score) do 26 | :telemetry.execute( 27 | @complexity_name, 28 | %{score: score}, 29 | %{query: query} 30 | ) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics/graphql/query_name.ex: -------------------------------------------------------------------------------- 1 | if PrometheusTelemetry.Utils.app_loaded?(:absinthe) do 2 | defmodule PrometheusTelemetry.Metrics.GraphQL.QueryName do 3 | @moduledoc false 4 | 5 | alias Absinthe.Blueprint 6 | alias Absinthe.Language.{ListType, NamedType, NonNullType} 7 | 8 | @regex_capture_operation ~r/(?query|mutation|subscription|)(\s*(?\w+)\(.*\)|)\s*{(?.*)}$/U 9 | @regex_capture_query ~r/\s*(?\w+)\(.*\)\s*({(?.*)}$|$)/U 10 | 11 | def capture_operation(%Blueprint{input: operation}) when is_binary(operation) do 12 | capture_valid_graphql_values(@regex_capture_operation, operation) 13 | end 14 | 15 | def capture_query(query) do 16 | capture_valid_graphql_values(@regex_capture_query, query) 17 | end 18 | 19 | def capture_query_name(query) do 20 | case capture_query(query) do 21 | %{"name" => name} -> name 22 | _ -> "Unknown" 23 | end 24 | end 25 | 26 | defp capture_valid_graphql_values(regex, string) do 27 | case Regex.named_captures(regex, String.replace(string, "\n", "")) do 28 | nil -> %{"name" => "Unknown by Regex"} 29 | matches -> Map.reject(matches, fn {_, value} -> value === "" end) 30 | end 31 | end 32 | 33 | def get_name(query) do 34 | case get_initial_definition(query) do 35 | %{name: name} = definition when not is_nil(name) -> 36 | variables = build_variables_with_types(definition) 37 | "#{definition_operation(definition)} #{name}#{variables}" 38 | 39 | %{selection_set: %{selections: [%{name: name} | _]}} = definition -> 40 | variables = build_variables_with_types(definition) 41 | "#{definition_operation(definition)} #{name || "Unknown"}#{variables}" 42 | 43 | _ -> 44 | "operation Unknown" 45 | end 46 | end 47 | 48 | defp definition_operation(%{operation: operation}) do 49 | operation 50 | end 51 | 52 | def get_query_name(query) do 53 | query 54 | |> get_initial_definition 55 | |> get_name_from_definition(fn definition, _ -> 56 | build_variables_with_types(definition) 57 | end) 58 | end 59 | 60 | def get_query_name(query, variables) do 61 | query 62 | |> get_initial_definition 63 | |> get_name_from_definition(variables, &build_variables_with_values/2) 64 | end 65 | 66 | defp get_name_from_definition(definition, variables \\ nil, variables_func) do 67 | case definition do 68 | %{name: name} = definition when not is_nil(name) -> 69 | variables = variables_func.(definition, variables) 70 | "#{name}#{variables}" 71 | 72 | %{selection_set: %{selections: [%{name: name} | _]}} = definition -> 73 | variables = variables_func.(definition, variables) 74 | "#{name || "Unknown"}#{variables}" 75 | 76 | _ -> 77 | "Unknown" 78 | end 79 | end 80 | 81 | 82 | defp get_initial_definition(query) do 83 | with {:ok, tokens} <- tokenize(query), 84 | {:ok, %{definitions: [definition | _]}} <- :absinthe_parser.parse(tokens) do 85 | definition 86 | end 87 | end 88 | 89 | defp tokenize(query) do 90 | Absinthe.Lexer.tokenize(query) 91 | end 92 | 93 | defp build_variables_with_types(definition) do 94 | variables = Enum.map_join(definition.variable_definitions, ", ", fn variable_definition -> 95 | variable = variable_definition.variable.name 96 | type = name_from_type(variable_definition.type) 97 | "$#{variable}: #{type}" 98 | end) 99 | 100 | if variables === "", do: "", else: "(#{variables})" 101 | end 102 | 103 | defp build_variables_with_values(definition, variables) do 104 | variables = 105 | Enum.map_join(definition.variable_definitions, ", ", fn %{variable: %{name: name}} -> 106 | "$#{name}: #{variables[name]}" 107 | end) 108 | 109 | if variables === "", do: "", else: "(#{variables})" 110 | end 111 | 112 | defp name_from_type(%NamedType{} = definition), do: definition.name 113 | defp name_from_type(%NonNullType{} = definition), do: name_from_type(definition.type) 114 | defp name_from_type(%ListType{} = definition), do: "[#{name_from_type(definition.type)}]" 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics/graphql/request.ex: -------------------------------------------------------------------------------- 1 | if PrometheusTelemetry.Utils.app_loaded?(:absinthe) do 2 | defmodule PrometheusTelemetry.Metrics.GraphQL.Request do 3 | @moduledoc false 4 | 5 | import Telemetry.Metrics, only: [counter: 2, distribution: 2] 6 | 7 | alias Absinthe.{Blueprint, Resolution} 8 | alias PrometheusTelemetry.Metrics.GraphQL.QueryName 9 | 10 | @buckets PrometheusTelemetry.Config.default_millisecond_buckets() 11 | @duration_unit {:native, :millisecond} 12 | 13 | @event_prefix [:absinthe] 14 | @requests_count @event_prefix ++ [:execute, :operation, :start] 15 | @response_count @event_prefix ++ [:execute, :operation, :stop] 16 | @execute_duration_name @event_prefix ++ [:execute, :operation, :stop] 17 | @subscription_duration_name @event_prefix ++ [:subscription, :publish, :stop] 18 | @resolve_duration_name @event_prefix ++ [:resolve, :field, :stop] 19 | @batch_middleware_duration_name @event_prefix ++ [:middleware, :batch, :stop] 20 | 21 | @type counter_type :: :subscription | :query | :mutation 22 | @type histogram_type :: :query | :mutation 23 | 24 | def metrics do 25 | [ 26 | counter( 27 | "graphql.requests.count", 28 | event_name: @requests_count, 29 | tags: [:type, :name], 30 | tag_values: &parse_name_and_type/1, 31 | measurement: :count, 32 | description: "Execute count for (query/mutations)" 33 | ), 34 | 35 | counter( 36 | "graphql.response.count", 37 | event_name: @response_count, 38 | tags: [:type, :name, :status], 39 | tag_values: &parse_name_and_type_and_response/1, 40 | measurement: :count, 41 | description: "Execute count for (query/mutations)" 42 | ), 43 | 44 | 45 | distribution( 46 | "graphql.execute.duration.milliseconds", 47 | event_name: @execute_duration_name, 48 | tags: [:type, :name], 49 | tag_values: &extract_name_and_type/1, 50 | measurement: :duration, 51 | unit: @duration_unit, 52 | reporter_options: [buckets: @buckets], 53 | description: "Execute duration for (query/mutations)" 54 | ), 55 | 56 | distribution( 57 | "graphql.subscription.duration.milliseconds", 58 | event_name: @subscription_duration_name, 59 | tags: [:name], 60 | tag_values: &extract_name_and_type/1, 61 | measurement: :duration, 62 | unit: @duration_unit, 63 | reporter_options: [buckets: @buckets], 64 | description: "Subscription duration for (query/mutations)" 65 | ), 66 | 67 | distribution( 68 | "graphql.resolve.duration.milliseconds", 69 | event_name: @resolve_duration_name, 70 | measurement: :duration, 71 | description: "Resolve duration for (query/mutations)", 72 | measurement: :duration, 73 | unit: @duration_unit, 74 | reporter_options: [buckets: @buckets], 75 | tags: [:type, :name], 76 | tag_values: &extract_resolve_name_and_type/1, 77 | drop: &drop_resolve_subscriptions/1 78 | ), 79 | 80 | distribution( 81 | "graphql.middleware.batch.duration.milliseconds", 82 | event_name: @batch_middleware_duration_name, 83 | tags: [:module, :function], 84 | tag_values: &extract_batch_name/1, 85 | measurement: :duration, 86 | unit: @duration_unit, 87 | reporter_options: [buckets: @buckets], 88 | description: "Middleware duration for batching" 89 | ) 90 | ] 91 | end 92 | 93 | defp parse_name_and_type_and_response(%{ 94 | blueprint: %{ 95 | result: %{ 96 | errors: [%{code: code} | _] 97 | } 98 | } 99 | } = metadata) do 100 | metadata 101 | |> extract_name_and_type 102 | |> Map.put(:status, PrometheusTelemetry.Utils.title_case(code)) 103 | end 104 | 105 | defp parse_name_and_type_and_response(%{ 106 | blueprint: %{ 107 | result: %{ 108 | errors: [_ | _] 109 | } 110 | } 111 | } = metadata) do 112 | metadata 113 | |> extract_name_and_type 114 | |> Map.put(:status, "Unknown Error") 115 | end 116 | 117 | defp parse_name_and_type_and_response(metadata) do 118 | metadata 119 | |> extract_name_and_type 120 | |> Map.put(:status, "Ok") 121 | end 122 | 123 | defp parse_name_and_type(%{blueprint: blueprint}) do 124 | case QueryName.capture_operation(blueprint) do 125 | %{"type" => type, "name" => name} -> %{type: to_string(type), name: name} 126 | %{"type" => type, "query" => query} -> %{type: to_string(type), name: QueryName.capture_query_name(query)} 127 | %{"query" => query} -> %{type: "query", name: QueryName.capture_query_name(query)} 128 | _ -> %{type: "Unknown", name: "Unknown"} 129 | end 130 | end 131 | 132 | defp extract_batch_name(%{batch_fun: batch_fun_tuple}) do 133 | [module, fnc_name | _] = Tuple.to_list(batch_fun_tuple) 134 | 135 | module = module |> to_string |> String.replace("Elixir.", "") 136 | %{module: module, function: to_string(fnc_name)} 137 | end 138 | 139 | defp extract_name_and_type(%{blueprint: blueprint, options: options}) do 140 | case Blueprint.current_operation(blueprint) do 141 | %{type: type, name: nil} -> %{type: to_string(type), name: query_name(options)} 142 | %{type: type, name: name} -> %{type: to_string(type), name: name} 143 | _ -> %{type: "Unknown", name: "Unknown"} 144 | end 145 | end 146 | 147 | defp extract_resolve_name_and_type(metadata) do 148 | %{type: query_or_mutation(metadata), name: query_name(metadata)} 149 | end 150 | 151 | defp drop_resolve_subscriptions(metadata) do 152 | query_or_mutation(metadata) === "subscription" 153 | end 154 | 155 | defp query_name(%{resolution: %Resolution{definition: %{name: name}}}) do 156 | name 157 | end 158 | 159 | defp query_name(opts) do 160 | if Keyword.has_key?(opts, :document) do 161 | QueryName.get_query_name(opts[:document]) 162 | else 163 | "Unknown" 164 | end 165 | end 166 | 167 | defp query_or_mutation(%{ 168 | resolution: %Resolution{definition: %{parent_type: %{identifier: identifier}}} 169 | }) do 170 | identifier 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics/oban.ex: -------------------------------------------------------------------------------- 1 | if PrometheusTelemetry.Utils.app_loaded?(:oban) do 2 | defmodule PrometheusTelemetry.Metrics.Oban do 3 | @moduledoc """ 4 | These metrics give you metrics around Oban jobs 5 | 6 | - `oban.job.started.count` 7 | - `oban.job.duration.millisecond` 8 | - `oban.job.queue_time.millisecond` 9 | - `oban.job.exception.duration.millisecond` 10 | - `oban.job.exception.queue_time.millisecond` 11 | """ 12 | 13 | import Telemetry.Metrics, only: [counter: 2, distribution: 2] 14 | 15 | @duration_unit {:native, :millisecond} 16 | @buckets PrometheusTelemetry.Config.default_millisecond_buckets() 17 | 18 | def metrics do 19 | [ 20 | counter("oban.job.started.count", 21 | event_name: [:oban, :job, :start], 22 | measurement: :count, 23 | description: "Oban jobs fetched count", 24 | tags: [:prefix, :queue, :attempt], 25 | tag_values: &extract_job_metadata/1 26 | ), 27 | 28 | distribution("oban.job.duration.millisecond", 29 | event_name: [:oban, :job, :stop], 30 | measurement: :duration, 31 | description: "Oban job duration", 32 | tags: [:prefix, :queue, :attempt, :state], 33 | tag_values: &extract_duration_metadata/1, 34 | unit: @duration_unit, 35 | reporter_options: [buckets: @buckets] 36 | ), 37 | 38 | distribution("oban.job.queue_time.millisecond", 39 | event_name: [:oban, :job, :stop], 40 | measurement: :queue_time, 41 | description: "Oban job queue time", 42 | tags: [:prefix, :queue, :attempt, :state], 43 | tag_values: &extract_duration_metadata/1, 44 | unit: @duration_unit, 45 | reporter_options: [buckets: @buckets] 46 | ), 47 | 48 | distribution("oban.job.exception.duration.millisecond", 49 | event_name: [:oban, :job, :exception], 50 | measurement: :duration, 51 | description: "Oban job exception duration", 52 | tags: [:prefix, :queue, :kind, :state, :reason], 53 | tag_values: &extract_exception_metadata/1, 54 | unit: @duration_unit, 55 | reporter_options: [buckets: @buckets] 56 | ), 57 | 58 | distribution("oban.job.exception.queue_time.millisecond", 59 | event_name: [:oban, :job, :exception], 60 | measurement: :queue_time, 61 | description: "Oban job exception queue time", 62 | tags: [:prefix, :queue, :kind, :state, :reason], 63 | tag_values: &extract_exception_metadata/1, 64 | unit: @duration_unit, 65 | reporter_options: [buckets: @buckets] 66 | ) 67 | ] 68 | end 69 | 70 | defp extract_job_metadata(metadata), do: Map.take(metadata, [:prefix, :queue, :attempt]) 71 | defp extract_duration_metadata(metadata), do: Map.take(metadata, [:prefix, :queue, :attempt, :state]) 72 | 73 | defp extract_exception_metadata(%{reason: reason} = metadata) do 74 | metadata 75 | |> Map.take([:prefix, :queue, :kind, :state]) 76 | |> Map.put(:reason, format_reason(reason)) 77 | end 78 | 79 | defp format_reason(%Oban.CrashError{}), do: "Crash Error" 80 | defp format_reason(%Oban.PerformError{}), do: "Perform Error" 81 | defp format_reason(%Oban.TimeoutError{}), do: "Timeout Error" 82 | defp format_reason(_), do: "Unknown" 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics/phoenix.ex: -------------------------------------------------------------------------------- 1 | if PrometheusTelemetry.Utils.app_loaded?(:phoenix) do 2 | defmodule PrometheusTelemetry.Metrics.Phoenix do 3 | @moduledoc """ 4 | These metrics give you metrics around phoenix requests 5 | 6 | - `http_request.duration.milliseconds` 7 | - `phoenix.endpoint_call.duration.milliseconds` 8 | - `phoenix.controller_call.duration.milliseconds` 9 | - `phoenix.controller_error_rendered.duration.milliseconds` 10 | - `phoenix.channel_join.duration.milliseconds_bucket` 11 | - `phoenix.channel_receive.duration.milliseconds` 12 | """ 13 | 14 | import Telemetry.Metrics, only: [counter: 2, distribution: 2] 15 | 16 | @duration_unit {:native, :millisecond} 17 | @buckets PrometheusTelemetry.Config.default_millisecond_buckets() 18 | 19 | def metrics do 20 | [ 21 | counter("phoenix.endpoint_call.count", 22 | event_name: [:phoenix, :endpoint, :start], 23 | measurement: :count, 24 | description: "Phoenix endpoint request count", 25 | tags: [:action, :controller, :status], 26 | keep: &has_action?/1, 27 | tag_values: &controller_metadata/1 28 | ), 29 | 30 | distribution("http_request.duration.milliseconds", 31 | event_name: [:phoenix, :endpoint, :stop], 32 | measurement: :duration, 33 | description: "HTTP Request duration", 34 | tags: [:status_class, :method, :host, :scheme], 35 | tag_values: &http_request_metadata/1, 36 | unit: @duration_unit, 37 | reporter_options: [buckets: @buckets] 38 | ), 39 | 40 | distribution("phoenix.endpoint_call.duration.milliseconds", 41 | event_name: [:phoenix, :endpoint, :stop], 42 | measurement: :duration, 43 | description: "Phoenix endpoint request time (inc middleware)", 44 | tags: [:action, :controller, :status], 45 | keep: &has_action?/1, 46 | tag_values: &controller_metadata/1, 47 | unit: @duration_unit, 48 | reporter_options: [buckets: @buckets] 49 | ), 50 | 51 | distribution("phoenix.controller_call.duration.milliseconds", 52 | event_name: [:phoenix, :router_dispatch, :stop], 53 | measurement: :duration, 54 | description: "Phoenix router request time", 55 | tags: [:action, :controller, :status], 56 | keep: &has_action?/1, 57 | tag_values: &controller_metadata/1, 58 | unit: @duration_unit, 59 | reporter_options: [buckets: @buckets] 60 | ), 61 | 62 | distribution("phoenix.controller_error_rendered.duration.milliseconds", 63 | event_name: [:phoenix, :error_rendered], 64 | measurement: :duration, 65 | description: "Phoenix controller error render time", 66 | tags: [:action, :controller, :status], 67 | keep: &has_action?/1, 68 | tag_values: &controller_metadata/1, 69 | unit: @duration_unit, 70 | reporter_options: [buckets: @buckets] 71 | ), 72 | 73 | distribution("phoenix.channel_join.duration.milliseconds", 74 | event_name: [:phoenix, :router_dispatch], 75 | measurement: :duration, 76 | description: "Phoenix router request time", 77 | tags: [:channel, :topic, :transport], 78 | tag_values: &socket_metadata/1, 79 | unit: @duration_unit, 80 | reporter_options: [buckets: @buckets] 81 | ), 82 | 83 | distribution("phoenix.channel_receive.duration.milliseconds", 84 | event_name: [:phoenix, :channel_handled_in], 85 | measurement: :duration, 86 | description: "Phoenix router request time", 87 | tags: [:channel, :topic, :event, :transport], 88 | tag_values: &socket_metadata/1, 89 | unit: @duration_unit, 90 | reporter_options: [buckets: @buckets] 91 | ) 92 | ] 93 | end 94 | 95 | defp has_action?(%{conn: %{private: %{phoenix_action: action}}}) when not is_nil(action) do 96 | true 97 | end 98 | 99 | defp has_action?(_), do: false 100 | 101 | defp controller_metadata(%{ 102 | conn: %{ 103 | status: status, 104 | private: private 105 | } 106 | }) do 107 | action = Map.get(private, :phoenix_action, "Unknown") 108 | controller = private |> Map.get(:phoenix_controller, "Unknown") |> module_name_to_string 109 | 110 | %{action: action, controller: controller, status: status} 111 | end 112 | 113 | defp socket_metadata(%{socket: socket} = metadata) do 114 | channel = socket |> Map.get(:channel, "Unknown") |> module_name_to_string 115 | transport = Map.get(socket, :transport, "Unknown") 116 | topic = Map.get(socket, :topic, "Unknown") 117 | 118 | %{channel: channel, topic: topic, transport: transport, event: metadata[:event]} 119 | end 120 | 121 | defp module_name_to_string(module) when is_binary(module) do 122 | module 123 | end 124 | 125 | defp module_name_to_string(module) do 126 | module 127 | |> to_string 128 | |> String.replace("Elixir.", "") 129 | end 130 | 131 | defp http_request_metadata(%{ 132 | conn: %{host: host, method: method, scheme: scheme, status: status} 133 | }) do 134 | %{status_class: status_class(status), method: method, host: host, scheme: scheme} 135 | end 136 | 137 | defp status_class(code) when code <= 100 or code >= 600 do 138 | "unknown" 139 | end 140 | 141 | defp status_class(code) when code < 200 do 142 | "informational" 143 | end 144 | 145 | defp status_class(code) when code < 300 do 146 | "success" 147 | end 148 | 149 | defp status_class(code) when code < 400 do 150 | "redirection" 151 | end 152 | 153 | defp status_class(code) when code < 500 do 154 | "client-error" 155 | end 156 | 157 | defp status_class(code) when code < 600 do 158 | "server-error" 159 | end 160 | 161 | defp status_class(code) do 162 | raise "Status code for phoenix_metric not an integer #{inspect(code)}" 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics/swoosh.ex: -------------------------------------------------------------------------------- 1 | if PrometheusTelemetry.Utils.app_loaded?(:swoosh) do 2 | defmodule PrometheusTelemetry.Metrics.Swoosh do 3 | @moduledoc """ 4 | These metrics give you metrics around swoosh emails 5 | 6 | - `swoosh.deliver.request_count` 7 | - `swoosh.deliver.request_duration` 8 | - `swoosh.deliver.exception_count` 9 | - `swoosh.deliver_many.request_count` 10 | - `swoosh.deliver_many.request_duration` 11 | - `swoosh.deliver_many.exception_count` 12 | """ 13 | 14 | require Logger 15 | 16 | import Telemetry.Metrics, only: [counter: 2, distribution: 2] 17 | 18 | @duration_unit {:native, :millisecond} 19 | @buckets PrometheusTelemetry.Config.default_millisecond_buckets() 20 | 21 | def metrics do 22 | [ 23 | counter("swoosh.deliver.request_count", 24 | event_name: [:swoosh, :deliver, :start], 25 | measurement: :count, 26 | description: "Swoosh delivery delivery count", 27 | tags: [:mailer, :status, :from_address], 28 | tag_values: fn metadata -> 29 | metadata 30 | |> add_status_to_metadata 31 | |> serialize_metadata 32 | end 33 | ), 34 | 35 | distribution("swoosh.deliver.request.duration.milliseconds", 36 | event_name: [:swoosh, :deliver, :stop], 37 | measurement: :duration, 38 | description: "Swoosh delivery duration", 39 | tags: [:mailer, :from_address], 40 | tag_values: &serialize_metadata/1, 41 | unit: @duration_unit, 42 | reporter_options: [buckets: @buckets] 43 | ), 44 | 45 | counter("swoosh.deliver.exception_count", 46 | event_name: [:swoosh, :deliver, :exception], 47 | measurement: :count, 48 | description: "Swoosh delivery delivery exception count", 49 | tags: [:mailer, :error, :from_address], 50 | tag_values: &serialize_metadata/1 51 | ), 52 | 53 | counter("swoosh.deliver_many.request.count", 54 | event_name: [:swoosh, :deliver_many, :start], 55 | measurement: :count, 56 | description: "Swoosh delivery many count", 57 | tags: [:mailer, :from_address], 58 | tag_values: &serialize_metadata/1 59 | ), 60 | 61 | distribution("swoosh.deliver_many.request.duration.milliseconds", 62 | event_name: [:swoosh, :deliver_many, :stop], 63 | measurement: :duration, 64 | description: "Swoosh delivery many duration", 65 | tags: [:mailer, :from_address], 66 | tag_values: &serialize_metadata/1, 67 | unit: @duration_unit, 68 | reporter_options: [buckets: @buckets] 69 | ) 70 | ] 71 | end 72 | 73 | defp serialize_metadata(metadata) do 74 | metadata 75 | |> stringify_mailer_metadata 76 | |> add_email_from_to_metadata 77 | end 78 | 79 | defp stringify_mailer_metadata(%{mailer: mailer_mod} = metadata) do 80 | %{metadata | mailer: inspect(mailer_mod)} 81 | end 82 | 83 | defp add_status_to_metadata(%{error: _} = metadata), do: Map.put(metadata, :status, "error") 84 | defp add_status_to_metadata(metadata), do: Map.put(metadata, :status, "success") 85 | 86 | defp add_email_from_to_metadata(%{email: %Swoosh.Email{from: {_, address}}} = metadata) do 87 | Map.put(metadata, :from_address, address) 88 | end 89 | 90 | defp add_email_from_to_metadata(%{email: %Swoosh.Email{from: address}} = metadata) do 91 | Map.put(metadata, :from_address, address) 92 | end 93 | 94 | defp add_email_from_to_metadata(metadata) do 95 | Map.put(metadata, :from_address, "Unknown") 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics/vm.ex: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.Metrics.VM do 2 | @moduledoc """ 3 | These metrics give you some basic VM statistics from erlang, this includes: 4 | 5 | - `erlang.vm.memory` 6 | - `erlang.vm.run_queue.total` 7 | - `erlang.vm.run_queue.cpu` 8 | - `erlang.vm.run_queue.io` 9 | """ 10 | 11 | import Telemetry.Metrics, only: [last_value: 2, sum: 2] 12 | 13 | def metrics do 14 | [ 15 | last_value( 16 | "erlang.vm.memory", 17 | event_name: [:vm, :memory], 18 | measurement: :total, 19 | unit: {:byte, :kilobyte} 20 | ), 21 | 22 | last_value( 23 | "erlang.vm.memory.ets", 24 | event_name: [:vm, :memory], 25 | measurement: :ets 26 | ), 27 | 28 | last_value( 29 | "erlang.vm.run_queue.total", 30 | event_name: [:vm, :total_run_queue_lengths], 31 | measurement: :total 32 | ), 33 | 34 | last_value( 35 | "erlang.vm.run_queue.cpu", 36 | event_name: [:vm, :total_run_queue_lengths], 37 | measurement: :cpu 38 | ), 39 | 40 | last_value( 41 | "erlang.vm.run_queue.io", 42 | event_name: [:vm, :total_run_queue_lengths], 43 | measurement: :io 44 | ), 45 | 46 | sum( 47 | "erlang_vm_uptime", 48 | event_name: [:erlang_vm_uptime], 49 | measurement: :uptime, 50 | description: "The continuous uptime of the Erlang VM" 51 | ), 52 | 53 | last_value( 54 | "erlang.vm.system_counts.process_count", 55 | event_name: [:vm, :system_counts], 56 | measurement: :process_count 57 | ), 58 | 59 | last_value( 60 | "erlang.vm.system_counts.port_count", 61 | event_name: [:vm, :system_counts], 62 | measurement: :port_count 63 | ), 64 | 65 | last_value( 66 | "erlang.vm.system_counts.atom_count", 67 | event_name: [:vm, :system_counts], 68 | measurement: :atom_count 69 | ) 70 | ] 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/metrics_exporter_plug.ex: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.MetricsExporterPlug do 2 | @moduledoc """ 3 | This is the exporter for prometheus that sets up metrics to be scraped 4 | 5 | It uses /metrics as a route and is setup on port 4050 by default 6 | """ 7 | 8 | def child_spec(opts) do 9 | Plug.Cowboy.child_spec( 10 | scheme: opts[:protocol], 11 | plug: PrometheusTelemetry.Router, 12 | options: [ip: opts[:ip], port: opts[:port]] 13 | ) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/periodic_measurements/erlang_vm.ex: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.PeriodicMeasurements.ErlangVM do 2 | @moduledoc """ 3 | Uses the :erlang.statistics method to check the uptime of the Erlang VM and emits a :telemetry event 4 | with the most recent reading. 5 | """ 6 | 7 | @event_name [:erlang_vm_uptime] 8 | 9 | @spec vm_wall_clock :: :ok 10 | def vm_wall_clock do 11 | {_, uptime} = :erlang.statistics(:wall_clock) 12 | :telemetry.execute(@event_name, %{uptime: uptime}) 13 | end 14 | 15 | @spec periodic_measurements :: [{module(), atom(), keyword()}] 16 | def periodic_measurements, do: [{__MODULE__, :vm_wall_clock, []}] 17 | end 18 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/router.ex: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.Router do 2 | @moduledoc false 3 | 4 | use Plug.Router 5 | 6 | import Plug.Conn, only: [put_resp_content_type: 2, send_resp: 3] 7 | 8 | plug :match 9 | plug Plug.Telemetry, event_prefix: [:prometheus_metrics, :plug] 10 | plug :dispatch 11 | 12 | get "/metrics" do 13 | metrics = PrometheusTelemetry.get_metrics_string() 14 | 15 | conn 16 | |> put_resp_content_type("text/plain") 17 | |> send_resp(200, metrics) 18 | end 19 | 20 | match _ do 21 | send_resp(conn, 404, "Not Found") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/prometheus_telemetry/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.Utils do 2 | @moduledoc false 3 | 4 | def app_loaded?(app_name) do 5 | ensure_application_loaded?(app_name) and 6 | Enum.any?(Application.loaded_applications(), fn {dep_name, _, _} -> dep_name === app_name end) 7 | end 8 | 9 | defp ensure_application_loaded?(app_name) do 10 | case Application.ensure_loaded(app_name) do 11 | :ok -> true 12 | _ -> false 13 | end 14 | end 15 | 16 | def title_case(atom) when is_atom(atom) do 17 | atom |> to_string |> title_case 18 | end 19 | 20 | def title_case(str) do 21 | str |> String.replace("_", " ") |> :string.titlecase 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :prometheus_telemetry, 7 | version: "0.4.13", 8 | elixir: "~> 1.12", 9 | description: "Prometheus metrics exporter using Telemetry.Metrics as a foundation", 10 | start_permanent: Mix.env() == :prod, 11 | elixirc_paths: elixirc_paths(Mix.env()), 12 | deps: deps(), 13 | docs: docs(), 14 | package: package(), 15 | preferred_cli_env: [ 16 | dialyzer: :test 17 | ], 18 | 19 | elixirc_options: [ 20 | warnings_as_errors: true 21 | ], 22 | 23 | dialyzer: [ 24 | plt_add_apps: [:ex_unit, :mix, :credo], 25 | list_unused_filters: true, 26 | plt_local_path: ".dialyzer", 27 | plt_core_path: ".dialyzer", 28 | ignore_warnings: ".dialyzer-ignore.exs", 29 | flags: [:unmatched_returns, :no_improper_lists] 30 | ] 31 | ] 32 | end 33 | 34 | # Run "mix help compile.app" to learn about applications. 35 | def application do 36 | [ 37 | extra_applications: (if Mix.env() === :prod, do: [:logger], else: [:logger, :eex]) 38 | ] 39 | end 40 | 41 | # Run "mix help deps" to learn about dependencies. 42 | defp deps do 43 | [ 44 | {:telemetry_metrics_prometheus_core, "~> 1.2"}, 45 | {:telemetry_metrics, "~> 1.0"}, 46 | {:telemetry_poller, "~> 1.0"}, 47 | {:nimble_options, "~> 1.0"}, 48 | 49 | {:absinthe, ">= 0.0.0", optional: true}, 50 | {:ecto, ">= 0.0.0", optional: true}, 51 | {:oban, ">= 0.0.0", optional: true}, 52 | {:phoenix, ">= 0.0.0", optional: true}, 53 | {:hackney, ">= 0.0.0", optional: true}, 54 | {:swoosh, ">= 0.0.0", optional: true}, 55 | {:finch, ">= 0.12.0", optional: true}, 56 | 57 | {:plug, "~> 1.8"}, 58 | {:plug_cowboy, "~> 2.5"}, 59 | 60 | {:faker, "~> 0.17", only: [:test, :dev]}, 61 | 62 | {:credo, "~> 1.6", only: [:test, :dev], runtime: false}, 63 | {:blitz_credo_checks, "~> 0.1", only: [:test, :dev], runtime: false}, 64 | 65 | {:ex_doc, ">= 0.0.0", optional: true, only: :dev}, 66 | {:dialyxir, "~> 1.0", optional: true, only: [:dev, :test], runtime: false} 67 | ] 68 | end 69 | 70 | defp package do 71 | [ 72 | maintainers: ["Mika Kalathil"], 73 | licenses: ["MIT"], 74 | links: %{"GitHub" => "https://github.com/theblitzapp/prometheus_telemetry_elixir"}, 75 | files: ~w(mix.exs README.md CHANGELOG.md LICENSE lib config priv) 76 | ] 77 | end 78 | 79 | defp docs do 80 | [ 81 | main: "PrometheusTelemetry", 82 | source_url: "https://github.com/theblitzapp/prometheus_telemetry_elixir", 83 | 84 | groups_for_modules: [ 85 | "General": [ 86 | PrometheusTelemetry, 87 | PrometheusTelemetry.MetricsExporterPlug 88 | ], 89 | 90 | "Metrics": [ 91 | PrometheusTelemetry.Metrics.Cowboy, 92 | PrometheusTelemetry.Metrics.Ecto, 93 | PrometheusTelemetry.Metrics.Finch, 94 | PrometheusTelemetry.Metrics.GraphQL, 95 | PrometheusTelemetry.Metrics.Oban, 96 | PrometheusTelemetry.Metrics.Phoenix, 97 | PrometheusTelemetry.Metrics.Swoosh, 98 | PrometheusTelemetry.Metrics.VM 99 | ] 100 | ] 101 | ] 102 | end 103 | 104 | defp elixirc_paths(:test), do: ["lib", "test/support"] 105 | defp elixirc_paths(_), do: ["lib"] 106 | end 107 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "absinthe": {:hex, :absinthe, "1.7.8", "43443d12ad2b4fcce60e257ac71caf3081f3d5c4ddd5eac63a02628bcaf5b556", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c4085df201892a498384f997649aedb37a4ce8a726c170d5b5617ed3bf45d40b"}, 3 | "blitz_credo_checks": {:hex, :blitz_credo_checks, "0.1.10", "54ae0aa673101e3edd4f2ab0ee7860f90c3240e515e268546dd9f01109d2b917", [:mix], [{:credo, "~> 1.4", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "b3248dd2c88a6fe907e84ed104e61b863c6451d8755aa609b36d3eb6c7bab9db"}, 4 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 5 | "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, 6 | "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, 7 | "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, 8 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, 9 | "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, 10 | "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"}, 11 | "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, 12 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, 13 | "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, 14 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 15 | "ecto": {:hex, :ecto, "3.12.1", "626765f7066589de6fa09e0876a253ff60c3d00870dd3a1cd696e2ba67bfceea", [: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", "df0045ab9d87be947228e05a8d153f3e06e0d05ab10c3b3cc557d2f7243d1940"}, 16 | "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"}, 17 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 18 | "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"}, 19 | "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, 20 | "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, 21 | "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, 22 | "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, 23 | "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, 24 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 25 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 26 | "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"}, 27 | "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"}, 28 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 29 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 30 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 31 | "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, 32 | "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, 33 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 34 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 35 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 36 | "oban": {:hex, :oban, "2.18.2", "583e78965ee15263ac968e38c983bad169ae55eadaa8e1e39912562badff93ba", [: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", "9dd25fd35883a91ed995e9fe516e479344d3a8623dfe2b8c3fc8e5be0228ec3a"}, 37 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 38 | "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, 39 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 40 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, 41 | "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, 42 | "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, 43 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, 44 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, 45 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 46 | "swoosh": {:hex, :swoosh, "1.16.12", "cbb24ad512f2f7f24c7a469661c188a00a8c2cd64e0ab54acd1520f132092dfd", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e262df1ae510d59eeaaa3db42189a2aa1b3746f73771eb2616fc3f7ee63cc20"}, 47 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 48 | "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, 49 | "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, 50 | "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, 51 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 52 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 53 | "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, 54 | } 55 | -------------------------------------------------------------------------------- /priv/metric_template.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module_name %> do 2 | import Telemetry.Metrics, only: <%= inspect(@metrics_imports) %> 3 | 4 | <%= if Enum.any?(@metrics, &(&1[:type] === :distribution)) do %> 5 | @buckets PrometheusTelemetry.Config.default_millisecond_buckets() 6 | <% end %> 7 | 8 | def metrics do 9 | [ 10 | <% last_metric = List.last(@metrics) %> 11 | <%= for metric <- @metrics do %> 12 | <%= to_string(metric[:type]) %>( 13 | "<%= metric[:metric_name] %>", 14 | event_name: <%= inspect(metric[:event_name]) %>, 15 | measurement: :<%= metric[:measurement] %><%= if metric[:tags] !== [] || metric[:unit] do %>,<% end %> 16 | <%= if metric[:unit] do %>unit: {:native, :<%= metric[:unit] %>}, 17 | reporter_options: [buckets: <%= "@buckets" %>]<%= if metric[:tags] !== [] do %>,<% end %><% end %> 18 | <%= if metric[:tags] !== [] do %>tags: <%= inspect(metric[:tags]) %><% end %> 19 | )<%= if metric !== last_metric do %>,<% end %> 20 | 21 | <% end %> 22 | ] 23 | end 24 | 25 | <%= for metric <- @metrics do %> 26 | <%= if metric[:type] === :counter do %> 27 | def inc_<%= metric[:event_function] %> do 28 | :telemetry.execute( 29 | <%= inspect(metric[:event_name]) %>, 30 | %{<%= metric[:measurement] %>: 1}, 31 | %{} 32 | ) 33 | end 34 | <% end %> 35 | 36 | <%= if metric[:type] === :distribution do %> 37 | def observe_<%= metric[:event_function] %>(value) do 38 | :telemetry.execute( 39 | <%= inspect(metric[:event_name]) %>, 40 | %{<%= metric[:measurement] %>: value}, 41 | %{} 42 | ) 43 | end 44 | <% end %> 45 | 46 | <%= if metric[:type] === :last_value do %> 47 | def set_<%= metric[:event_function] %>(value) do 48 | :telemetry.execute( 49 | <%= inspect(metric[:event_name]) %>, 50 | %{<%= metric[:measurement] %>: value}, 51 | %{} 52 | ) 53 | end 54 | <% end %> 55 | 56 | <%= if metric[:type] === :sum do %> 57 | def observe_<%= metric[:event_function] %>(value) do 58 | :telemetry.execute( 59 | <%= inspect(metric[:event_name]) %>, 60 | %{<%= metric[:measurement] %>: value}, 61 | %{} 62 | ) 63 | end 64 | <% end %> 65 | <% end %> 66 | end 67 | -------------------------------------------------------------------------------- /test/prometheus_telemetry/metrics/ecto_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.Metrics.EctoTest do 2 | use ExUnit.Case, async: true 3 | alias PrometheusTelemetry.Metrics.Ecto 4 | 5 | describe "metrics/1" do 6 | setup _ do 7 | Module.create(MyEctoApp.Repo, nil, Macro.Env.location(__ENV__)) 8 | 9 | :ok 10 | end 11 | 12 | test "rejects module as prefix" do 13 | assert_raise ArgumentError, ~r/^expects an atom or a string prefix/, fn -> 14 | Ecto.metrics(MyEctoApp.Repo) 15 | end 16 | 17 | assert_raise ArgumentError, ~r/^expects an atom or a string prefix/, fn -> 18 | Ecto.metrics([MyEctoApp.Repo, MyEctoApp.Repo.Replica1]) 19 | end 20 | end 21 | end 22 | 23 | describe "metrics_for_repos/1" do 24 | test "able to create a list of metrics" do 25 | metrics_length = length(Ecto.metrics(:asdf)) 26 | 27 | repos = [Repo, Repo.CMS] 28 | assert length(Ecto.metrics_for_repos(repos)) === length(repos) * metrics_length 29 | end 30 | 31 | test "able to reduce replica repo into one" do 32 | metrics_length = length(Ecto.metrics(:asdf)) 33 | 34 | secondary_replicas = [ 35 | Repo.Replica2, 36 | Repo.LeaguePlayer.Replica2 37 | ] 38 | 39 | repos = [ 40 | Repo, 41 | Repo.Replica1, 42 | Repo.LeaguePlayer.Replica1 43 | ] 44 | 45 | metrics = Ecto.metrics_for_repos(repos ++ secondary_replicas) 46 | 47 | # Should have 8 metrics 48 | # 4 for regular repo 49 | # 4 for replicas 50 | assert length(metrics) === length(repos) * metrics_length 51 | end 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /test/prometheus_telemetry/periodic_measurements/erlang_vm_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.PeriodicMeasurements.ErlangVMTest do 2 | use ExUnit.Case 3 | 4 | alias PrometheusTelemetry.PeriodicMeasurements.ErlangVM 5 | 6 | @event_name [:erlang_vm_uptime] 7 | 8 | doctest ErlangVM 9 | 10 | describe "vm_wall_clock/0" do 11 | setup do: %{self: self()} 12 | 13 | test "writes value to telemetry", %{self: self, test: test} do 14 | PrometheusTelemetry.TestHelpers.start_telemetry_listener(self, test, @event_name) 15 | 16 | ErlangVM.vm_wall_clock() 17 | 18 | assert_receive {:telemetry_event, @event_name, %{uptime: _}, _metadata} 19 | end 20 | end 21 | 22 | describe "periodic_measurements/0" do 23 | test "returns expected list" do 24 | assert [{ErlangVM, :vm_wall_clock, []}] = ErlangVM.periodic_measurements() 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/prometheus_telemetry/router_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.RouterTest do 2 | use ExUnit.Case, async: true 3 | 4 | @test_port 4050 5 | @finch_name RouterTest.Finch 6 | 7 | setup do 8 | assert {:ok, _} = Finch.start_link(name: @finch_name) 9 | :ok 10 | end 11 | 12 | describe "&fetch_metrics/0" do 13 | test "fetches and renders metric data" do 14 | assert {:ok, %Finch.Response{body: body}} = 15 | :get 16 | |> Finch.build("http://localhost:#{@test_port}") 17 | |> Finch.request(@finch_name) 18 | 19 | assert String.length(body) > 1 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/prometheus_telemetry_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetryTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Telemetry.Metrics, only: [counter: 2] 5 | 6 | @event_name_a [:some_event, :name] 7 | @event_name_b [:some_event, :name, :secondary] 8 | 9 | setup_all do 10 | key = generate_key() 11 | name = String.to_atom(key) 12 | 13 | {:ok, pid} = 14 | PrometheusTelemetry.start_link( 15 | name: name, 16 | metrics: metrics(key) 17 | ) 18 | 19 | %{pid: pid, key: key, name: name} 20 | end 21 | 22 | @moduletag capture_log: true 23 | 24 | describe "&start_link/1" do 25 | test "can start multiple metrics under a supervisor", %{key: key, name: name} do 26 | assert {:ok, _pid} = 27 | PrometheusTelemetry.start_link( 28 | name: name, 29 | metrics: new_metrics(key) 30 | ) 31 | end 32 | 33 | test "raise error if metrics and periodic_measurements is nil" do 34 | assert_raise NimbleOptions.ValidationError, fn -> 35 | PrometheusTelemetry.start_link( 36 | periodic_measurements: nil, 37 | metrics: nil 38 | ) 39 | end 40 | end 41 | 42 | test "can have multiple supervisors under separate names to group metrics", %{pid: pid} do 43 | key = generate_key() 44 | 45 | assert {:ok, new_pid} = 46 | PrometheusTelemetry.start_link( 47 | name: String.to_atom(key), 48 | metrics: metrics(key) 49 | ) 50 | 51 | assert new_pid !== pid 52 | end 53 | 54 | test "starts telemetry_poller as a child" do 55 | key = generate_key() 56 | 57 | assert {:ok, _new_pid} = PrometheusTelemetry.start_link( 58 | name: String.to_atom(key), 59 | periodic_measurements: periodic_measurements(key) 60 | ) 61 | 62 | [actual_name | _] = Enum.filter(Process.registered(), &String.ends_with?(to_string(&1), "poller")) 63 | actual_name = Atom.to_string(actual_name) 64 | 65 | assert String.starts_with?(actual_name, key) 66 | assert String.ends_with?(actual_name, PrometheusTelemetry.poller_postfix()) 67 | end 68 | end 69 | 70 | describe "&list/0" do 71 | test "lists all supervisors", %{key: key} do 72 | new_key = generate_key() 73 | 74 | assert {:ok, _new_pid} = 75 | PrometheusTelemetry.start_link( 76 | name: String.to_atom(new_key), 77 | metrics: metrics(new_key) 78 | ) 79 | 80 | supervisor_length = 81 | PrometheusTelemetry.list() 82 | |> Enum.filter(fn supervisor_name -> 83 | string_name = Atom.to_string(supervisor_name) 84 | 85 | string_name =~ key or string_name =~ new_key 86 | end) 87 | |> length() 88 | 89 | assert supervisor_length === 2 90 | end 91 | end 92 | 93 | describe "&list_prometheus_cores/1" do 94 | test "lists all the metric cores from a supervisor", %{pid: pid} do 95 | assert [prometheus_core | _] = PrometheusTelemetry.list_prometheus_cores(pid) 96 | 97 | assert prometheus_core |> TelemetryMetricsPrometheus.Core.scrape() |> is_binary() 98 | end 99 | end 100 | 101 | defp metrics(key) do 102 | [ 103 | counter("#{key}.some_thing.test", 104 | event_name: @event_name_a, 105 | measurement: :count, 106 | description: "HELLO" 107 | ) 108 | ] 109 | end 110 | 111 | defp periodic_measurements(_key) do 112 | [ 113 | { 114 | PrometheusTelemetryTest, 115 | :measurement_method, 116 | [] 117 | } 118 | ] 119 | end 120 | 121 | defp new_metrics(key) do 122 | [ 123 | counter("#{key}.some_thing.test.version_2", 124 | event_name: @event_name_b, 125 | measurement: :count, 126 | description: "HELLO22", 127 | tag: [:second_item] 128 | ) 129 | ] 130 | end 131 | 132 | defp generate_key, do: String.replace(Faker.Pokemon.name(), ~r/[^\d\w]/, "") 133 | end 134 | -------------------------------------------------------------------------------- /test/support/mock_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.Support.MockSupervisor do 2 | @moduledoc "gen server use for mock metric" 3 | 4 | import Telemetry.Metrics, only: [counter: 2] 5 | 6 | @event_name_b "magic_snort_snort" 7 | 8 | @spec setup :: any 9 | def setup do 10 | PrometheusTelemetry.start_link( 11 | name: :test_exporter, 12 | exporter: [enabled?: true], 13 | metrics: [ 14 | counter("some_thing.test.magic", 15 | event_name: @event_name_b, 16 | measurement: :count, 17 | description: "HELLO", 18 | tag: [:bacon] 19 | ) 20 | ] 21 | ) 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /test/support/test_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule PrometheusTelemetry.TestHelpers do 2 | @moduledoc """ 3 | Provides helper methods for testing :telemetry events. 4 | 5 | Calling start_telemetry_listener/4 registers the event_handler/1 method which echoes the telemetry event 6 | back to the containing module. 7 | 8 | This allows the use of assert_receive/3 to confirm the event was fired. 9 | 10 | Example: 11 | 12 | @event_name [:my_counter] 13 | 14 | describe "my_event_executioner/0" do 15 | test "counter event is executed", %{self: self, test: test} do 16 | Telemetry.TestHelpers.start_telemetry_listener(self, test, @event_name) 17 | 18 | OUT.my_event_executioner() 19 | 20 | assert_receive({:telemetry_event, @event_name, %{: _}, _metadata}) 21 | end 22 | end 23 | """ 24 | 25 | @spec start_telemetry_listener(module(), atom(), [atom()], map()) :: :ok | {:error, :already_exists} 26 | def start_telemetry_listener(send_dest, handler_id, event_name, config \\ %{}) do 27 | :telemetry.attach( 28 | handler_id, 29 | event_name, 30 | &PrometheusTelemetry.TestHelpers.event_handler(send_dest, &1, &2, &3, &4), 31 | config 32 | ) 33 | end 34 | 35 | @spec event_handler(module(), term, map, map, map) :: function() 36 | def event_handler(dest, name, measurements, metadata, _config) do 37 | send(dest, {:telemetry_event, name, measurements, metadata}) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | PrometheusTelemetry.Support.MockSupervisor.setup() 2 | 3 | ExUnit.start() 4 | --------------------------------------------------------------------------------