From 7a5802cb142d26d98518b51a7e0755ec74ac5ade Mon Sep 17 00:00:00 2001 From: Bryan Naegele Date: Wed, 23 Mar 2022 13:02:28 -0600 Subject: [PATCH] Process propagator library (#64) * Process propagator library * Fix Elixir API * CI files * Update propagators/opentelemetry_process_propagator/lib/opentelemetry_process_propagator.ex Co-authored-by: Andrew Rosa * Update propagators/opentelemetry_process_propagator/lib/opentelemetry_process_propagator.ex Co-authored-by: Andrew Rosa * format Co-authored-by: Andrew Rosa --- .github/labeler.yml | 9 +- .github/workflows/elixir.yml | 37 ++++ .../.formatter.exs | 5 + .../.gitignore | 19 ++ .../opentelemetry_process_propagator/LICENSE | 191 ++++++++++++++++++ .../README.md | 6 + .../lib/opentelemetry_process_propagator.ex | 115 +++++++++++ .../opentelemetry_process_propagator/mix.exs | 100 +++++++++ .../opentelemetry_process_propagator/mix.lock | 21 ++ .../rebar.config | 29 +++ .../rebar.lock | 8 + .../opentelemetry_process_propagator.app.src | 15 ++ .../src/opentelemetry_process_propagator.erl | 66 ++++++ .../opentelemetry_process_propagator_test.exs | 161 +++++++++++++++ .../test/test_helper.exs | 1 + 15 files changed, 780 insertions(+), 3 deletions(-) create mode 100644 propagators/opentelemetry_process_propagator/.formatter.exs create mode 100644 propagators/opentelemetry_process_propagator/.gitignore create mode 100644 propagators/opentelemetry_process_propagator/LICENSE create mode 100644 propagators/opentelemetry_process_propagator/README.md create mode 100644 propagators/opentelemetry_process_propagator/lib/opentelemetry_process_propagator.ex create mode 100644 propagators/opentelemetry_process_propagator/mix.exs create mode 100644 propagators/opentelemetry_process_propagator/mix.lock create mode 100644 propagators/opentelemetry_process_propagator/rebar.config create mode 100644 propagators/opentelemetry_process_propagator/rebar.lock create mode 100644 propagators/opentelemetry_process_propagator/src/opentelemetry_process_propagator.app.src create mode 100644 propagators/opentelemetry_process_propagator/src/opentelemetry_process_propagator.erl create mode 100644 propagators/opentelemetry_process_propagator/test/opentelemetry_process_propagator_test.exs create mode 100644 propagators/opentelemetry_process_propagator/test/test_helper.exs diff --git a/.github/labeler.yml b/.github/labeler.yml index a0c7464..66c232a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -53,14 +53,17 @@ opentelemetry_cowboy: opentelemetry_ecto: - instrumentation/opentelemetry_ecto/**/* +opentelemetry_oban: + - instrumentation/opentelemetry_oban/**/* + opentelemetry_phoenix: - instrumentation/opentelemetry_phoenix/**/* +opentelemetry_process_propagator: + - propagators/opentelemetry_process_propagator/**/* + opentelemetry_redix: - instrumentation/opentelemetry_redix/**/* opentelemetry_telemetry: - utilities/opentelemetry_telemetry/**/* - -opentelemetry_oban: - - instrumentation/opentelemetry_oban/**/* diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 8911373..c9040b4 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -228,3 +228,40 @@ jobs: run: mix format --check-formatted - name: Test run: mix test + + opentelemetry-process-propagator: + needs: [test-matrix] + if: (contains(github.event.pull_request.labels.*.name, 'elixir') && contains(github.event.pull_request.labels.*.name, 'opentelemetry_process_propagator')) + env: + app: 'opentelemetry_process_propagator' + defaults: + run: + working-directory: propagators/${{ env.app }} + runs-on: ubuntu-18.04 + name: Opentelemetry Process Propagator 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@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 diff --git a/propagators/opentelemetry_process_propagator/.formatter.exs b/propagators/opentelemetry_process_propagator/.formatter.exs new file mode 100644 index 0000000..3ebaf3c --- /dev/null +++ b/propagators/opentelemetry_process_propagator/.formatter.exs @@ -0,0 +1,5 @@ +# Used by "mix format" +[ + line_length: 120, + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/propagators/opentelemetry_process_propagator/.gitignore b/propagators/opentelemetry_process_propagator/.gitignore new file mode 100644 index 0000000..f1c4554 --- /dev/null +++ b/propagators/opentelemetry_process_propagator/.gitignore @@ -0,0 +1,19 @@ +.rebar3 +_* +.eunit +*.o +*.beam +*.plt +*.swp +*.swo +.erlang.cookie +ebin +log +erl_crash.dump +.rebar +logs +_build +.idea +*.iml +rebar3.crashdump +*~ diff --git a/propagators/opentelemetry_process_propagator/LICENSE b/propagators/opentelemetry_process_propagator/LICENSE new file mode 100644 index 0000000..d665b3c --- /dev/null +++ b/propagators/opentelemetry_process_propagator/LICENSE @@ -0,0 +1,191 @@ + 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 + + Copyright 2021, Bryan Naegele . + + 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/propagators/opentelemetry_process_propagator/README.md b/propagators/opentelemetry_process_propagator/README.md new file mode 100644 index 0000000..5442edd --- /dev/null +++ b/propagators/opentelemetry_process_propagator/README.md @@ -0,0 +1,6 @@ +# Opentelemetry Process Propagator + +[![EEF Observability WG project](https://img.shields.io/badge/EEF-Observability-black)](https://github.com/erlef/eef-observability-wg) +[![Hex.pm](https://img.shields.io/hexpm/v/opentelemetry_process_propagator)](https://hex.pm/packages/opentelemetry_process_propagator) + +Functions for cross-process opentelemetry context propagation. diff --git a/propagators/opentelemetry_process_propagator/lib/opentelemetry_process_propagator.ex b/propagators/opentelemetry_process_propagator/lib/opentelemetry_process_propagator.ex new file mode 100644 index 0000000..53aa2e2 --- /dev/null +++ b/propagators/opentelemetry_process_propagator/lib/opentelemetry_process_propagator.ex @@ -0,0 +1,115 @@ +defmodule OpentelemetryProcessPropagator do + @moduledoc """ + `OpentelemetryProcessPropagator` provides helpers for dealing + with context propagation across process boundaries. + + ## Context Propagation + + Erlang and Elixir do not have a mechanism for transparently passing + context between processes. This requires the user to explicitly + pass data between processes. In order to continue a trace across + processes, the user must start a new span and pass it to the + spawned process. + + ``` + span_ctx = OpenTelemetry.Tracer.start_span("child") + ctx = OpenTelemetry.Ctx.get_current() + + task = + Task.async(fn -> + OpenTelemetry.Ctx.attach(ctx) + OpenTelemetry.Tracer.set_current_span(span_ctx) + # do work here + + OpenTelemetry.Tracer.end_span(span_ctx) + end) + + _result = Task.await(task) + ``` + + ### Reverse Propagation + + It's not always possible to have full control over traces, such as + when using `telemetry` events emitted from a library you don't control + to create a span. In such cases, a mechanism to fetch a context from a + calling process is necessary. This is effectively context propagation + in reverse. + + As an example, Ecto uses the `Task` module to execute preloads which are + each a separate query. Since a task is a spawned process, creating an otel + span results in orphan spans. To correctly connect these spans we must + find the otel context which spawned the process. + + ## Usage + + Example of using `fetch_parent_ctx/1` to find a parent context. + + ```elixir + OpenTelemetry.with_span :span_started_in_your_app do + # some span being created in a process spawned by a library + # you don't control, e.g. Ecto preloads + + Task.async(fn -> + parent_ctx = OpentelemetryProcessPropagator.fetch_parent_ctx(:"$callers") + + OpenTelemetry.Ctx.attach(parent_ctx) + + attrs = %{some_attr: :from_telemetry_event} + + span = + OpenTelemetry.Tracer.start_span(:span_created_in_lib, %{attributes: attrs}) + + OpenTelemetry.Span.end_span(span) + end) + end + ``` + + ```erlang + ?with_span(span_started_in_your_app, #{}, fun() -> + %% some span being created in a process spawned by a library + %% you don't control + + proc_lib:spawn_link(fun() -> + Tracer = opentelemetry:get_tracer(test_tracer), + ParentCtx = opentelemetry_process_propagator:fetch_parent_ctx(), + otel_ctx:attach(ParentCtx), + Span = otel_tracer:start_span(Tracer, span_created_in_lib, #{}), + otel_tracer:end_span(Span). + ). + end + ``` + + """ + + @doc """ + Attempt to fetch an otel context from a give pid. + """ + @spec fetch_ctx(pid) :: OpenTelemetry.span_ctx() | :undefined + defdelegate fetch_ctx(pid), to: :opentelemetry_process_propagator + + @doc """ + Attempt to find an otel context in the spawning process. + + This is equivalent to calling `fetch_parent_ctx(1, :"$ancestors")` + """ + @spec fetch_parent_ctx() :: OpenTelemetry.span_ctx() | :undefined + defdelegate fetch_parent_ctx(), to: :opentelemetry_process_propagator + + @doc """ + Attempt to find an otel context in a spawning process within `n` number of parent + processes + """ + @spec fetch_parent_ctx(non_neg_integer()) :: OpenTelemetry.span_ctx() | :undefined + defdelegate fetch_parent_ctx(depth), to: :opentelemetry_process_propagator + + @doc """ + Attempt to find an otel context under a given process dictionary key + within `n` number of parent processes. The first context found will be + returned. + + Processes spawned by `proc_lib` are stored under `:"$ancestors`. The + Elixir `Task` module uses the `:"$callers` key. + """ + @spec fetch_parent_ctx(non_neg_integer(), atom()) :: OpenTelemetry.span_ctx() | :undefined + defdelegate fetch_parent_ctx(max_depth, key), to: :opentelemetry_process_propagator +end diff --git a/propagators/opentelemetry_process_propagator/mix.exs b/propagators/opentelemetry_process_propagator/mix.exs new file mode 100644 index 0000000..ec913ef --- /dev/null +++ b/propagators/opentelemetry_process_propagator/mix.exs @@ -0,0 +1,100 @@ +defmodule OpentelemetryProcessPropagator.MixProject do + use Mix.Project + + def project do + {app, desc} = load_app() + config = load_config() + + [ + app: app, + version: version(Keyword.fetch!(desc, :vsn)), + description: to_string(Keyword.fetch!(desc, :description)), + elixir: "~> 1.10", + start_permanent: Mix.env() == :prod, + deps: deps(Keyword.fetch!(config, :deps)), + name: "Opentelemetry Process Propagator", + source_url: + "https://github.com/open-telemetry/opentelemetry-erlang-contrib/tree/main/propagators/opentelemetry_process_propagator", + docs: [ + markdown_processor: ExDoc.Markdown.Earmark, + main: "OpentelemetryTelemetry", + # logo: "path/to/logo.png", + # erlang_docs() + extras: [] + ], + aliases: [ + # when build docs first build edocs with rebar3 + docs: ["cmd rebar3 edoc", "docs"] + ], + package: package() + ] + end + + defp version(version) when is_list(version) do + List.to_string(version) + end + + defp version({:file, path}) do + path + |> File.read!() + |> String.trim() + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + defp deps(rebar) do + rebar + |> Enum.map(fn + {dep, version} -> {dep, to_string(version)} + dep when is_atom(dep) -> {dep, ">= 0.0.0"} + end) + |> Enum.concat([ + {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}, + {:ex_doc, "~> 0.26.0", only: :dev, runtime: false}, + {:opentelemetry, "~> 1.0", only: [:dev, :test]}, + {:opentelemetry_exporter, "~> 1.0", only: [:dev, :test]} + ]) + end + + defp package() do + [ + description: "Tools for opentelemetry context propagation across process boundaries", + build_tools: ["rebar3", "mix"], + files: ~w(lib mix.exs README.md LICENSE rebar.config rebar.lock VERSION src), + licenses: ["Apache-2.0"], + links: %{ + "GitHub" => + "https://github.com/open-telemetry/opentelemetry-erlang-contrib/tree/main/propagators/opentelemetry_process_propagator", + "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 erlang_docs() do + files = + for file <- Path.wildcard("edoc/*.md"), + file != "edoc/README.md", + do: {String.to_atom(file), [title: Path.basename(file, ".md")]} + + [{:"README.md", [title: "Overview"]} | files] + end + + defp load_config do + {:ok, config} = :file.consult('rebar.config') + + config + end + + defp load_app do + {:ok, [{:application, name, desc}]} = :file.consult('src/opentelemetry_process_propagator.app.src') + + {name, desc} + end +end diff --git a/propagators/opentelemetry_process_propagator/mix.lock b/propagators/opentelemetry_process_propagator/mix.lock new file mode 100644 index 0000000..522e6ff --- /dev/null +++ b/propagators/opentelemetry_process_propagator/mix.lock @@ -0,0 +1,21 @@ +%{ + "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.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [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", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"}, + "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.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, + "opentelemetry": {:hex, :opentelemetry, "1.0.0", "6e98f4a9230681b2e4c88d45783ce1c02d671ffc0b5ac0cba69a34a3f5ada8d8", [:rebar3], [{:opentelemetry_api, "~> 1.0.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "08d8697740f70594d05067cb62a0a8845ff568b2d47e1f8c78c46708ab58a74f"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.0.0", "6e501f750ead189f35aed07eb8023fa6655fca12f913a196102f67db4ed5172c", [:mix, :rebar3], [], "hexpm", "ac51520bde21fdea7f82cea9236ce4e88a21281c22bc23b0f1fa3b28b4352fcf"}, + "opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.0.0", "12fd928e72dec8108d40214a667e7a90026827d6268db678617c9e40ec3dc931", [: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", "e2d2377abfb823cc99c1b68b4ce31df1ff1ce63e0c7bdbee7a7527e2d825168d"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "tls_certificate_check": {:hex, :tls_certificate_check, "1.11.0", "609dcd503f31170f0799dac380eb0e086388cf918fc769aaa60ddd6bbf575218", [:rebar3], [{:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "4ab962212ef7c87482619cb298e1fe06e047b57f0bd58cc417b3b299eb8d036e"}, +} diff --git a/propagators/opentelemetry_process_propagator/rebar.config b/propagators/opentelemetry_process_propagator/rebar.config new file mode 100644 index 0000000..fa31b9d --- /dev/null +++ b/propagators/opentelemetry_process_propagator/rebar.config @@ -0,0 +1,29 @@ +{erl_opts, [debug_info]}. +{deps, [ + {opentelemetry_api, "~> 1.0"} +]}. + +{project_plugins, [covertool, + erlfmt]}. +{profiles, + [{docs, [{deps, [edown]}, + {edoc_opts, + [{doclet, edown_doclet}, + {preprocess, true}, + {dir, "edoc"}, + {subpackages, true}]}]}, + {test, [{erl_opts, [nowarn_export_all]}, + {deps, [ + {opentelemetry, "~> 1.0"}, + {opentelemetry_exporter, "~> 1.0"} + ]}, + {paths, ["src", "test/support"]}, + {ct_opts, [{ct_hooks, [cth_surefire]}]}]}]}. + +{xref_checks, [undefined_function_calls, undefined_functions, + deprecated_function_calls, deprecated_functions]}. +{xref_ignores, []}. + +{cover_enabled, true}. +{cover_export_enabled, true}. +{covertool, [{coverdata_files, ["ct.coverdata"]}]}. diff --git a/propagators/opentelemetry_process_propagator/rebar.lock b/propagators/opentelemetry_process_propagator/rebar.lock new file mode 100644 index 0000000..f68702e --- /dev/null +++ b/propagators/opentelemetry_process_propagator/rebar.lock @@ -0,0 +1,8 @@ +{"1.2.0", +[{<<"opentelemetry_api">>,{pkg,<<"opentelemetry_api">>,<<"1.0.0-rc.4">>},0}]}. +[ +{pkg_hash,[ + {<<"opentelemetry_api">>, <<"13FB20B8800149CE52DAA8FB793C1A45B826CC4BD0D5BC7E8A00CAB46285CBE6">>}]}, +{pkg_hash_ext,[ + {<<"opentelemetry_api">>, <<"29C5BF7D5C40C4B9816906277BEB2438E6737073721C75398465B1ACD1B56B06">>}]} +]. diff --git a/propagators/opentelemetry_process_propagator/src/opentelemetry_process_propagator.app.src b/propagators/opentelemetry_process_propagator/src/opentelemetry_process_propagator.app.src new file mode 100644 index 0000000..9b38488 --- /dev/null +++ b/propagators/opentelemetry_process_propagator/src/opentelemetry_process_propagator.app.src @@ -0,0 +1,15 @@ +{application, opentelemetry_process_propagator, + [{description, "Tools for OpenTelemetry context propagation across process boundaries"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, + [kernel, + stdlib, + opentelemetry_api + ]}, + {env,[]}, + {modules, []}, + + {licenses, ["Apache 2.0"]}, + {links, []} + ]}. diff --git a/propagators/opentelemetry_process_propagator/src/opentelemetry_process_propagator.erl b/propagators/opentelemetry_process_propagator/src/opentelemetry_process_propagator.erl new file mode 100644 index 0000000..05c0a49 --- /dev/null +++ b/propagators/opentelemetry_process_propagator/src/opentelemetry_process_propagator.erl @@ -0,0 +1,66 @@ +-module(opentelemetry_process_propagator). + +-export([fetch_ctx/1, + fetch_parent_ctx/0, + fetch_parent_ctx/1, + fetch_parent_ctx/2]). + +-spec fetch_parent_ctx() -> opentelemetry:span_ctx() | undefined. +fetch_parent_ctx() -> + fetch_parent_ctx(1, '$ancestors'). + +-spec fetch_parent_ctx(non_neg_integer()) -> opentelemetry:span_ctx() | undefined. +fetch_parent_ctx(MaxDepth) -> + fetch_parent_ctx(MaxDepth, '$ancestors'). + +-spec fetch_parent_ctx(non_neg_integer(), atom()) -> opentelemetry:span_ctx() | undefined. +fetch_parent_ctx(MaxDepth, Key) -> + Pids = pids(Key, pdict(self())), + inspect_parent(undefined, lists:sublist(Pids, MaxDepth)). + +inspect_parent(Ctx, _Pids) when Ctx =/= undefined -> + Ctx; +inspect_parent(Ctx, []) -> + Ctx; +inspect_parent(_Ctx, [Pid | Rest]) -> + case fetch_ctx(Pid) of + undefined -> + inspect_parent(undefined, Rest); + OtelCtx -> + inspect_parent(OtelCtx, []) + end. + +-spec fetch_ctx(pid()) -> opentelemetry:span_ctx() | undefined. +fetch_ctx(Pid) -> + case pdict(Pid) of + undefined -> + undefined; + Dictionary -> + otel_ctx(Dictionary) + end. + +-spec pdict(pid()) -> [{term(), term()}] | undefined. +pdict(Pid) -> + case process_info(Pid, dictionary) of + {dictionary, Dict} -> + Dict; + undefined -> + undefined + end. + +-spec otel_ctx([{term(), term()}]) -> opentelemetry:span_ctx() | undefined. +otel_ctx(Dictionary) -> + case lists:keyfind('$__current_otel_ctx', 1, Dictionary) of + false -> + undefined; + {'$__current_otel_ctx', Ctx} -> + Ctx + end. + +pids(Key, Dictionary) -> + case lists:keyfind(Key, 1, Dictionary) of + false -> + []; + {Key,Pids} -> + Pids + end. diff --git a/propagators/opentelemetry_process_propagator/test/opentelemetry_process_propagator_test.exs b/propagators/opentelemetry_process_propagator/test/opentelemetry_process_propagator_test.exs new file mode 100644 index 0000000..9d74e77 --- /dev/null +++ b/propagators/opentelemetry_process_propagator/test/opentelemetry_process_propagator_test.exs @@ -0,0 +1,161 @@ +defmodule OpentelemetryProcessPropagatorTest do + use ExUnit.Case + import OpentelemetryProcessPropagator + + require OpenTelemetry.Tracer, as: Tracer + alias OpenTelemetry.Ctx + + setup_all do + Application.put_env(:opentelemetry, :processors, + otel_batch_processor: %{scheduled_delay_ms: 1, exporter: [otel_exporter_pid: self()]} + ) + end + + describe "fetch_ctx/1" do + test "returns undefined if no context found" do + assert :undefined == fetch_ctx(self()) + end + + test "returns the ctx if there is one" do + span_ctx = Tracer.start_span("test") + Tracer.set_current_span(span_ctx) + + ctx = Ctx.get_current() + + assert ctx == fetch_ctx(self()) + end + end + + describe "fetch_parent_ctx/0" do + test "will not find parent ctx when spawned by kernel" do + ctx = Tracer.start_span("test") + Tracer.set_current_span(ctx) + + pid = self() + + spawn(fn -> + send(pid, fetch_parent_ctx()) + end) + + assert_receive :undefined + end + + test "fetches the parent ctx when spawned by proclib" do + span_ctx = Tracer.start_span("test") + Tracer.set_current_span(span_ctx) + + ctx = Ctx.get_current() + + pid = self() + + :proc_lib.spawn(fn -> + p_ctx = fetch_parent_ctx() + send(pid, p_ctx) + end) + + assert_receive ^ctx + end + end + + describe "fetch_parent_ctx/1" do + test "will not find parent ctx when parent beyond max depth" do + span_ctx = Tracer.start_span("test") + Tracer.set_current_span(span_ctx) + + pid = self() + + :proc_lib.spawn(fn -> + :proc_lib.spawn(fn -> + send(pid, fetch_parent_ctx()) + end) + end) + + assert_receive :undefined + end + + test "fetches the parent ctx when within max depth" do + span_ctx = Tracer.start_span("test") + Tracer.set_current_span(span_ctx) + + ctx = Ctx.get_current() + + pid = self() + + :proc_lib.spawn(fn -> + :proc_lib.spawn(fn -> + send(pid, fetch_parent_ctx(2)) + end) + end) + + assert_receive ^ctx + end + + test "fetches the first ctx found" do + span_ctx1 = Tracer.start_span("parent") + Tracer.set_current_span(span_ctx1) + + ctx = Ctx.get_current() + pid = self() + + span_ctx2 = Tracer.start_span("child") + + :proc_lib.spawn(fn -> + Ctx.attach(ctx) + Tracer.set_current_span(span_ctx2) + + Ctx.get_current() + + :proc_lib.spawn(fn -> + send(pid, fetch_parent_ctx(2)) + end) + + # keep proc alive + Process.sleep(10) + end) + + refute_receive ^ctx + end + end + + describe "fetch_parent_ctx/2" do + test "works with other keys" do + span_ctx = Tracer.start_span("test") + Tracer.set_current_span(span_ctx) + + ctx = Ctx.get_current() + + pid = self() + + Task.async(fn -> + send(pid, fetch_parent_ctx(1, :"$callers")) + end) + + assert_receive ^ctx + end + end + + defmodule GenServerTest do + use GenServer + + def init(_), do: {:ok, %{}} + + def handle_call(:call, {pid, _ref}, state) do + send(pid, fetch_parent_ctx()) + + {:reply, :ok, state} + end + end + + test "works with otp behaviours" do + {:ok, pid} = GenServer.start_link(GenServerTest, []) + + span_ctx = Tracer.start_span("test") + Tracer.set_current_span(span_ctx) + + ctx = Ctx.get_current() + + GenServer.call(pid, :call) + + assert_receive ^ctx + end +end diff --git a/propagators/opentelemetry_process_propagator/test/test_helper.exs b/propagators/opentelemetry_process_propagator/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/propagators/opentelemetry_process_propagator/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()