From dfed96874fe4fe666bf8da03da4cf426253f8b91 Mon Sep 17 00:00:00 2001 From: Andrew Rosa Date: Wed, 14 Dec 2022 21:38:18 -0300 Subject: [PATCH] Add Nebulex instrumentation library (#83) Add instrumentation for Nebulex, a distributed cache library. This library provides solid telemetry support for this initial implementation. Caching implementation is mostly based on in-memory storage (like ETS) and RPC calls for distribution (via OTP libraries, like :erpc). AFAICT, there is not much specifics for how to translate into Semantic Attributes: those caches are not quite a DB, except maybe for the one which implements the storage; the RPC can't be reliably captured either. Given the above constraints, this initial implementation instruments the library via custom attributes (namespaced as `nebulex.*`). It's not 100% clear the behaviour of OTel for actual distributed caches - from my tests, that may create some orphan spans. I think that's fine as first release. Nebulex follow the patterns of Ecto, so this instrumentation follows a similar pattern of OpentelemetryEcto. It does include a `setup_all/1` function for convenience, that leverages the :init events Nebulex emit on process start. Co-authored-by: Tristan Sloughter --- .github/labeler.yml | 3 + .github/workflows/elixir.yml | 36 +++ CODEOWNERS | 1 + .../opentelemetry_nebulex/.formatter.exs | 4 + .../opentelemetry_nebulex/.gitignore | 23 ++ .../opentelemetry_nebulex/CHANGELOG.md | 5 + instrumentation/opentelemetry_nebulex/LICENSE | 201 ++++++++++++++++ .../opentelemetry_nebulex/README.md | 29 +++ .../opentelemetry_nebulex/config/config.exs | 34 +++ .../opentelemetry_nebulex/config/test.exs | 4 + .../lib/opentelemetry_nebulex.ex | 111 +++++++++ instrumentation/opentelemetry_nebulex/mix.exs | 59 +++++ .../opentelemetry_nebulex/mix.lock | 25 ++ .../test/opentelemetry_nebulex_test.exs | 215 ++++++++++++++++++ .../test/test_helper.exs | 1 + 15 files changed, 751 insertions(+) create mode 100644 instrumentation/opentelemetry_nebulex/.formatter.exs create mode 100644 instrumentation/opentelemetry_nebulex/.gitignore create mode 100644 instrumentation/opentelemetry_nebulex/CHANGELOG.md create mode 100644 instrumentation/opentelemetry_nebulex/LICENSE create mode 100644 instrumentation/opentelemetry_nebulex/README.md create mode 100644 instrumentation/opentelemetry_nebulex/config/config.exs create mode 100644 instrumentation/opentelemetry_nebulex/config/test.exs create mode 100644 instrumentation/opentelemetry_nebulex/lib/opentelemetry_nebulex.ex create mode 100644 instrumentation/opentelemetry_nebulex/mix.exs create mode 100644 instrumentation/opentelemetry_nebulex/mix.lock create mode 100644 instrumentation/opentelemetry_nebulex/test/opentelemetry_nebulex_test.exs create mode 100644 instrumentation/opentelemetry_nebulex/test/test_helper.exs diff --git a/.github/labeler.yml b/.github/labeler.yml index b60948d..ceb593a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -53,6 +53,9 @@ opentelemetry_cowboy: opentelemetry_ecto: - instrumentation/opentelemetry_ecto/**/* +opentelemetry_nebulex: + - instrumentation/opentelemetry_nebulex/**/* + opentelemetry_oban: - instrumentation/opentelemetry_oban/**/* diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index dddf4aa..60371e3 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -49,7 +49,43 @@ jobs: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: opentelemetry_ecto_test + steps: + - uses: actions/checkout@v2 + - uses: erlef/setup-beam@v1 + with: + otp-version: ${{ matrix.otp_version }} + elixir-version: ${{ matrix.elixir_version }} + rebar3-version: ${{ matrix.rebar3_version }} + - name: Cache + uses: actions/cache@v2 + with: + path: | + ~/deps + ~/_build + key: ${{ runner.os }}-build-${{ matrix.otp_version }}-${{ matrix.elixir_version }}-v3-${{ hashFiles('**/mix.lock') }} + - name: Fetch deps + if: steps.deps-cache.outputs.cache-hit != 'true' + run: mix deps.get + - name: Compile project + run: mix compile --warnings-as-errors + - name: Check formatting + run: mix format --check-formatted + - name: Test + run: mix test + opentelemetry-nebulex: + needs: [test-matrix] + if: (contains(github.event.pull_request.labels.*.name, 'elixir') && contains(github.event.pull_request.labels.*.name, 'opentelemetry_nebulex')) + env: + app: 'opentelemetry_nebulex' + defaults: + run: + working-directory: instrumentation/${{ env.app }} + runs-on: ubuntu-18.04 + name: Opentelemetry Nebulex test on Elixir ${{ matrix.elixir_version }} (OTP ${{ matrix.otp_version }}) + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.test-matrix.outputs.matrix) }} steps: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 diff --git a/CODEOWNERS b/CODEOWNERS index 3397119..c17ea27 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -16,6 +16,7 @@ /instrumentation/opentelemetry_cowboy @bryannaegele @tsloughter /instrumentation/opentelemetry_ecto @bryannaegele @tsloughter +/instrumentation/opentelemetry_nebulex @andrewhr /instrumentation/opentelemetry_oban @indrekj /instrumentation/opentelemetry_phoenix @bryannaegele @tsloughter /instrumentation/opentelemetry_redix @andrewhr diff --git a/instrumentation/opentelemetry_nebulex/.formatter.exs b/instrumentation/opentelemetry_nebulex/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/instrumentation/opentelemetry_nebulex/.gitignore b/instrumentation/opentelemetry_nebulex/.gitignore new file mode 100644 index 0000000..946f1f8 --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/.gitignore @@ -0,0 +1,23 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +opentelemetry_redix-*.tar diff --git a/instrumentation/opentelemetry_nebulex/CHANGELOG.md b/instrumentation/opentelemetry_nebulex/CHANGELOG.md new file mode 100644 index 0000000..e1f3f91 --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +* Initial release diff --git a/instrumentation/opentelemetry_nebulex/LICENSE b/instrumentation/opentelemetry_nebulex/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/instrumentation/opentelemetry_nebulex/README.md b/instrumentation/opentelemetry_nebulex/README.md new file mode 100644 index 0000000..ad05379 --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/README.md @@ -0,0 +1,29 @@ +# OpentelemetryNebulex + +OpentelemetryNebulex uses `telemetry` handlers to create `OpenTelemetry` spans +from Nebulex command events. + +## Installation + +The package can be installed by adding `opentelemetry_nebulex` to your list of +dependencies in `mix.exs`: + +```elixir + def deps do + [ + {:opentelemetry_nebulex, "~> 0.1.0"} + ] + end +``` + +## Compatibility Matrix + +| OpentelemetryNebulex Version | Otel Version | Notes | +| :--------------------------- | :----------- | :---- | +| | | | +| v0.1.0 | v1.0.0 | | + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/opentelemetry_nebulex](https://hexdocs.pm/opentelemetry_nebulex). + diff --git a/instrumentation/opentelemetry_nebulex/config/config.exs b/instrumentation/opentelemetry_nebulex/config/config.exs new file mode 100644 index 0000000..19795c4 --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/config/config.exs @@ -0,0 +1,34 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +import Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# third-party users, it should be done in your "mix.exs" file. + +# You can configure your application as: +# +# config :opentelemetry_nebulex, key: :value +# +# and access this configuration in your application as: +# +# Application.get_env(:opentelemetry_nebulex, :key) +# +# You can also configure a third-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +try do + import_config "#{Mix.env()}.exs" +rescue + _ -> :ok +end diff --git a/instrumentation/opentelemetry_nebulex/config/test.exs b/instrumentation/opentelemetry_nebulex/config/test.exs new file mode 100644 index 0000000..28a095d --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/config/test.exs @@ -0,0 +1,4 @@ +import Config + +config :opentelemetry, + processors: [{:otel_simple_processor, %{}}] diff --git a/instrumentation/opentelemetry_nebulex/lib/opentelemetry_nebulex.ex b/instrumentation/opentelemetry_nebulex/lib/opentelemetry_nebulex.ex new file mode 100644 index 0000000..cd7856f --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/lib/opentelemetry_nebulex.ex @@ -0,0 +1,111 @@ +defmodule OpentelemetryNebulex do + @moduledoc """ + OpentelemetryNebulex uses `telemetry` handlers to create `OpenTelemetry` spans + from Nebulex command events. + """ + + @tracer_id __MODULE__ + + @doc """ + Initializes and configures telemetry handlers for a given cache. + + Example: + + OpentelemetryNebulex.setup([:blog, :partitioned_cache]) + """ + def setup(event_prefix, opts \\ []) do + :telemetry.attach( + {__MODULE__, event_prefix, :command_start}, + event_prefix ++ [:command, :start], + &__MODULE__.handle_command_start/4, + opts + ) + + :telemetry.attach( + {__MODULE__, event_prefix, :command_stop}, + event_prefix ++ [:command, :stop], + &__MODULE__.handle_command_stop/4, + opts + ) + + :telemetry.attach( + {__MODULE__, event_prefix, :command_exception}, + event_prefix ++ [:command, :exception], + &__MODULE__.handle_command_exception/4, + opts + ) + end + + @doc """ + Initializes and configures telemetry handlers for all caches. + + Use the `[:nebulex, :cache, :init]` event to automatically discover caches, and attach + the handlers dynamically. It only works for caches that start after this function is called. + + Example: + + OpentelemetryNebulex.setup_all() + """ + def setup_all(opts \\ []) do + :telemetry.attach( + __MODULE__, + [:nebulex, :cache, :init], + &__MODULE__.handle_init/4, + opts + ) + end + + @doc false + def handle_init(_event, _measurements, metadata, config) do + setup(metadata[:opts][:telemetry_prefix], config) + end + + @doc false + def handle_command_start(_event, _measurements, metadata, _config) do + span_name = "nebulex #{metadata.function_name}" + + attributes = + %{ + "nebulex.cache": metadata.adapter_meta.cache + } + |> maybe_put(:"nebulex.backend", metadata.adapter_meta[:backend]) + |> maybe_put(:"nebulex.keyslot", metadata.adapter_meta[:keyslot]) + |> maybe_put(:"nebulex.model", metadata.adapter_meta[:model]) + + OpentelemetryTelemetry.start_telemetry_span(@tracer_id, span_name, metadata, %{ + attributes: attributes + }) + end + + @doc false + def handle_command_stop(_event, _measurements, metadata, _config) do + ctx = OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, metadata) + + if action = extract_action(metadata) do + OpenTelemetry.Span.set_attribute(ctx, :"nebulex.action", action) + end + + OpentelemetryTelemetry.end_telemetry_span(@tracer_id, metadata) + end + + @doc false + def handle_command_exception(_event, _measurements, metadata, _config) do + ctx = OpentelemetryTelemetry.set_current_telemetry_span(@tracer_id, metadata) + + OpenTelemetry.Span.record_exception(ctx, metadata.reason, metadata.stacktrace) + OpenTelemetry.Tracer.set_status(OpenTelemetry.status(:error, format_error(metadata.reason))) + + OpentelemetryTelemetry.end_telemetry_span(@tracer_id, metadata) + end + + defp maybe_put(attributes, _key, nil), do: attributes + defp maybe_put(attributes, key, value), do: Map.put(attributes, key, value) + + defp extract_action(%{function_name: f, result: :"$expired"}) when f in [:get, :take], do: :miss + defp extract_action(%{function_name: f, result: nil}) when f in [:get, :take], do: :miss + defp extract_action(%{function_name: f, result: _}) when f in [:get, :take], do: :hit + defp extract_action(_), do: nil + + defp format_error(exception) when is_exception(exception), do: Exception.message(exception) + defp format_error(error), do: inspect(error) +end diff --git a/instrumentation/opentelemetry_nebulex/mix.exs b/instrumentation/opentelemetry_nebulex/mix.exs new file mode 100644 index 0000000..20f926b --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/mix.exs @@ -0,0 +1,59 @@ +defmodule OpentelemetryNebulex.MixProject do + use Mix.Project + + def project do + [ + app: :opentelemetry_nebulex, + description: description(), + version: "0.1.0", + elixir: "~> 1.11", + start_permanent: Mix.env() == :prod, + deps: deps(), + elixirc_paths: elixirc_paths(Mix.env()), + package: package(), + source_url: + "https://github.com/open-telemetry/opentelemetry-erlang-contrib/tree/main/instrumentation/opentelemetry_nebulex" + ] + end + + defp description do + "OpenTelemetry tracing for Nebulex" + end + + defp package do + [ + files: ~w(lib .formatter.exs mix.exs README* LICENSE* CHANGELOG*), + licenses: ["Apache-2.0"], + links: %{ + "GitHub" => + "https://github.com/open-telemetry/opentelemetry-erlang-contrib/tree/main/instrumentation/opentelemetry_nebulex", + "OpenTelemetry Erlang" => "https://github.com/open-telemetry/opentelemetry-erlang", + "OpenTelemetry Erlang Contrib" => + "https://github.com/open-telemetry/opentelemetry-erlang-contrib", + "OpenTelemetry.io" => "https://opentelemetry.io" + } + ] + end + + def application do + [ + extra_applications: [] + ] + end + + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + defp deps do + [ + {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}, + {:ex_doc, "~> 0.28.0", only: [:dev], runtime: false}, + {:nebulex, "~> 2.1", only: [:dev, :test]}, + {:opentelemetry, "~> 1.0", only: [:dev, :test]}, + {:opentelemetry_api, "~> 1.0"}, + {:opentelemetry_exporter, "~> 1.0", only: [:dev, :test]}, + {:opentelemetry_telemetry, "~> 1.0"}, + {:telemetry, "~> 0.4 or ~> 1.0"} + ] + end +end diff --git a/instrumentation/opentelemetry_nebulex/mix.lock b/instrumentation/opentelemetry_nebulex/mix.lock new file mode 100644 index 0000000..8894efa --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/mix.lock @@ -0,0 +1,25 @@ +%{ + "acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"}, + "chatterbox": {:hex, :ts_chatterbox, "0.11.0", "b8f372c706023eb0de5bf2976764edb27c70fe67052c88c1f6a66b3a5626847f", [:rebar3], [{:hpack, "~>0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "722fe2bad52913ab7e87d849fc6370375f0c961ffb2f0b5e6d647c9170c382a6"}, + "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, + "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.25", "2024618731c55ebfcc5439d756852ec4e85978a39d0d58593763924d9a15916f", [:mix], [], "hexpm", "56749c5e1c59447f7b7a23ddb235e4b3defe276afc220a6227237f3efe83f51e"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.28.4", "001a0ea6beac2f810f1abc3dbf4b123e9593eaa5f00dd13ded024eae7c523298", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bf85d003dd34911d89c8ddb8bda1a958af3471a274a4c2150a9c01c78ac3f8ed"}, + "gproc": {:hex, :gproc, "0.8.0", "cea02c578589c61e5341fce149ea36ccef236cc2ecac8691fba408e7ea77ec2f", [:rebar3], [], "hexpm", "580adafa56463b75263ef5a5df4c86af321f68694e7786cb057fd805d1e2a7de"}, + "grpcbox": {:hex, :grpcbox, "0.14.0", "3eb321bcd2275baf8b54cf381feb7b0559a50c02544de28fda039c7f2f9d1a7a", [:rebar3], [{:acceptor_pool, "~>1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~>0.11.0", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~>0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~>0.8.0", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "e24159b7b6d3f9869bbe528845c0125fed2259366ba908fd04a1f45fe81d0660"}, + "hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm", "06f580167c4b8b8a6429040df36cc93bba6d571faeaec1b28816523379cbb23a"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "nebulex": {:hex, :nebulex, "2.3.2", "74d8b54e867ca58930edf14de7cc35056cf1ae802bdbc64b3c8c5336234cb1ca", [:mix], [{:decorator, "~> 1.4", [hex: :decorator, repo: "hexpm", optional: true]}, {:shards, "~> 1.0", [hex: :shards, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "09e7c2e687a9d4da2cb6bdde90de74170450f17684f7cc5691faa213c5aa5b13"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "opentelemetry": {:hex, :opentelemetry, "1.0.3", "0d04f8f2c8b45c75cd7a6b31c0e3699f00bf82feee610f97f10971ddbcbb2010", [:rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "1e94db9989276f24c3ce9f0df2f46074a42f3f6c19057a2c1a6f863b6a1f1463"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.0.3", "77f9644c42340cd8b18c728cde4822ed55ae136f0d07761b78e8c54da46af93a", [:mix, :rebar3], [], "hexpm", "4293e06bd369bc004e6fad5edbb56456d891f14bd3f9f1772b18f1923e0678ea"}, + "opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.0.4", "60a64c75633a82b6c36a20043be355ac72a7b9b21633edd47407924c5596dde0", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.0", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.11", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "61da65290fbb6cac3459b84b8cd630795bf608df93a2b2cc49251cae78200e5e"}, + "opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.0.0", "d5982a319e725fcd2305b306b65c18a86afdcf7d96821473cf0649ff88877615", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.0", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "3401d13a1d4b7aa941a77e6b3ec074f0ae77f83b5b2206766ce630123a9291a9"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "telemetry_registry": {:hex, :telemetry_registry, "0.3.0", "6768f151ea53fc0fbca70dbff5b20a8d663ee4e0c0b2ae589590e08658e76f1e", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "492e2adbc609f3e79ece7f29fec363a97a2c484ac78a83098535d6564781e917"}, + "tls_certificate_check": {:hex, :tls_certificate_check, "1.14.0", "6d1638d56ac68b25c987d401dffb7cd059281339aadc3f8bf27ab33ee19ddbfe", [:rebar3], [{:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "b4452ddd3ae89cd84451afa0e218cb3ccd5178fe3c1de7fabcbddb12a137bcf4"}, +} diff --git a/instrumentation/opentelemetry_nebulex/test/opentelemetry_nebulex_test.exs b/instrumentation/opentelemetry_nebulex/test/opentelemetry_nebulex_test.exs new file mode 100644 index 0000000..08663fe --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/test/opentelemetry_nebulex_test.exs @@ -0,0 +1,215 @@ +defmodule OpentelemetryNebulexTest do + use ExUnit.Case, async: false + + doctest OpentelemetryNebulex + + require OpenTelemetry.Tracer + require OpenTelemetry.Span + require Record + + for {name, spec} <- Record.extract_all(from_lib: "opentelemetry/include/otel_span.hrl") do + Record.defrecord(name, spec) + end + + for {name, spec} <- Record.extract_all(from_lib: "opentelemetry_api/include/opentelemetry.hrl") do + Record.defrecord(name, spec) + end + + defmodule Local do + use Nebulex.Cache, + otp_app: :opentelemetry_nebulex, + adapter: Nebulex.Adapters.Local + end + + defmodule Partitioned do + use Nebulex.Cache, + otp_app: :opentelemetry_nebulex, + adapter: Nebulex.Adapters.Partitioned + end + + defmodule Multilevel do + use Nebulex.Cache, + otp_app: :opentelemetry_nebulex, + adapter: Nebulex.Adapters.Multilevel + + defmodule L1 do + use Nebulex.Cache, + otp_app: :opentelemetry_nebulex, + adapter: Nebulex.Adapters.Local + end + + defmodule L2 do + use Nebulex.Cache, + otp_app: :opentelemetry_nebulex, + adapter: Nebulex.Adapters.Partitioned + end + end + + setup do + :otel_simple_processor.set_exporter(:otel_exporter_pid, self()) + + OpenTelemetry.Tracer.start_span("test") + + on_exit(fn -> + OpenTelemetry.Tracer.end_span() + end) + end + + test "local cache commands" do + OpentelemetryNebulex.setup([:opentelemetry_nebulex_test, :local]) + + start_supervised!(Local) + + # miss + Local.get(:my_key) + + assert_receive {:span, span(name: "nebulex get", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.action": :miss, + "nebulex.backend": :ets, + "nebulex.cache": OpentelemetryNebulexTest.Local + } = :otel_attributes.map(attributes) + + # write + Local.put(:my_key, 42) + + assert_receive {:span, span(name: "nebulex put", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.backend": :ets, + "nebulex.cache": OpentelemetryNebulexTest.Local + } = :otel_attributes.map(attributes) + + # hit + Local.get(:my_key) + + assert_receive {:span, span(name: "nebulex get", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.action": :hit, + "nebulex.backend": :ets, + "nebulex.cache": OpentelemetryNebulexTest.Local + } = :otel_attributes.map(attributes) + end + + test "partitioned cache commands" do + OpentelemetryNebulex.setup([:opentelemetry_nebulex_test, :partitioned]) + OpentelemetryNebulex.setup([:opentelemetry_nebulex_test, :partitioned, :primary]) + + start_supervised!(Partitioned) + + # miss + Partitioned.get(:my_key) + + assert_receive {:span, span(name: "nebulex get", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.action": :miss, + "nebulex.backend": :ets, + "nebulex.cache": OpentelemetryNebulexTest.Partitioned.Primary + } = :otel_attributes.map(attributes) + + assert_receive {:span, span(name: "nebulex get", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.action": :miss, + "nebulex.cache": OpentelemetryNebulexTest.Partitioned, + "nebulex.keyslot": Nebulex.Adapters.Partitioned + } = :otel_attributes.map(attributes) + + # write + Partitioned.put(:my_key, 42) + + assert_receive {:span, span(name: "nebulex put", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.backend": :ets, + "nebulex.cache": OpentelemetryNebulexTest.Partitioned.Primary + } = :otel_attributes.map(attributes) + + assert_receive {:span, span(name: "nebulex put", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.cache": OpentelemetryNebulexTest.Partitioned, + "nebulex.keyslot": Nebulex.Adapters.Partitioned + } = :otel_attributes.map(attributes) + + # hit + Partitioned.get(:my_key) + + assert_receive {:span, span(name: "nebulex get", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.action": :hit, + "nebulex.backend": :ets, + "nebulex.cache": OpentelemetryNebulexTest.Partitioned.Primary + } = :otel_attributes.map(attributes) + + assert_receive {:span, span(name: "nebulex get", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.action": :hit, + "nebulex.cache": OpentelemetryNebulexTest.Partitioned, + "nebulex.keyslot": Nebulex.Adapters.Partitioned + } = :otel_attributes.map(attributes) + end + + test "multi-level cache commands" do + OpentelemetryNebulex.setup([:opentelemetry_nebulex_test, :multilevel]) + OpentelemetryNebulex.setup([:opentelemetry_nebulex_test, :multilevel, :l1]) + OpentelemetryNebulex.setup([:opentelemetry_nebulex_test, :multilevel, :l2]) + + start_supervised!( + {Multilevel, + [ + levels: [ + {Multilevel.L1, []}, + {Multilevel.L2, []} + ] + ]} + ) + + # write + Multilevel.put(:my_key, 42) + + assert_receive {:span, span(name: "nebulex put", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.backend": :ets, + "nebulex.cache": OpentelemetryNebulexTest.Multilevel.L1 + } = :otel_attributes.map(attributes) + + assert_receive {:span, span(name: "nebulex put", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.cache": OpentelemetryNebulexTest.Multilevel.L2, + "nebulex.keyslot": Nebulex.Adapters.Partitioned + } = :otel_attributes.map(attributes) + + assert_receive {:span, span(name: "nebulex put", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.cache": OpentelemetryNebulexTest.Multilevel + } = :otel_attributes.map(attributes) + + # hit + Multilevel.get(:my_key) + + assert_receive {:span, span(name: "nebulex get", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.action": :hit, + "nebulex.backend": :ets, + "nebulex.cache": OpentelemetryNebulexTest.Multilevel.L1 + } = :otel_attributes.map(attributes) + + assert_receive {:span, span(name: "nebulex get", kind: :internal, attributes: attributes)} + + assert %{ + "nebulex.action": :hit, + "nebulex.cache": OpentelemetryNebulexTest.Multilevel, + "nebulex.model": :inclusive + } = :otel_attributes.map(attributes) + end +end diff --git a/instrumentation/opentelemetry_nebulex/test/test_helper.exs b/instrumentation/opentelemetry_nebulex/test/test_helper.exs new file mode 100644 index 0000000..6a0af57 --- /dev/null +++ b/instrumentation/opentelemetry_nebulex/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start(capture_log: true)