From 71e1e4ab48369b58a1c97224bfffd79096042fdd Mon Sep 17 00:00:00 2001 From: Bryan Naegele Date: Wed, 6 Oct 2021 13:36:22 -0600 Subject: [PATCH] Cowboy instrumentation (#18) * Cowboy instrumentation --- ...st-matrix.json => elixir-test-matrix.json} | 0 .github/erlang-test-matrix.json | 19 ++ .github/labeler.yml | 11 +- .github/workflows/erlang.yml | 53 ++++ CODEOWNERS | 3 +- README.md | 3 +- .../opentelemetry_cowboy/.gitignore | 19 ++ instrumentation/opentelemetry_cowboy/LICENSE | 191 +++++++++++++ .../opentelemetry_cowboy/README.md | 33 +++ .../opentelemetry_cowboy/rebar.config | 32 +++ .../opentelemetry_cowboy/rebar.lock | 19 ++ .../src/opentelemetry_cowboy.app.src | 17 ++ .../src/opentelemetry_cowboy.erl | 125 +++++++++ .../test/opentelemetry_cowboy_SUITE.erl | 255 ++++++++++++++++++ .../opentelemetry_cowboy/test/test_h.erl | 23 ++ 15 files changed, 796 insertions(+), 7 deletions(-) rename .github/{test-matrix.json => elixir-test-matrix.json} (100%) create mode 100644 .github/erlang-test-matrix.json create mode 100644 .github/workflows/erlang.yml create mode 100644 instrumentation/opentelemetry_cowboy/.gitignore create mode 100644 instrumentation/opentelemetry_cowboy/LICENSE create mode 100644 instrumentation/opentelemetry_cowboy/README.md create mode 100644 instrumentation/opentelemetry_cowboy/rebar.config create mode 100644 instrumentation/opentelemetry_cowboy/rebar.lock create mode 100644 instrumentation/opentelemetry_cowboy/src/opentelemetry_cowboy.app.src create mode 100644 instrumentation/opentelemetry_cowboy/src/opentelemetry_cowboy.erl create mode 100644 instrumentation/opentelemetry_cowboy/test/opentelemetry_cowboy_SUITE.erl create mode 100644 instrumentation/opentelemetry_cowboy/test/test_h.erl diff --git a/.github/test-matrix.json b/.github/elixir-test-matrix.json similarity index 100% rename from .github/test-matrix.json rename to .github/elixir-test-matrix.json diff --git a/.github/erlang-test-matrix.json b/.github/erlang-test-matrix.json new file mode 100644 index 0000000..aabc044 --- /dev/null +++ b/.github/erlang-test-matrix.json @@ -0,0 +1,19 @@ +{ + "otp_version": [ + "24.0.6", + "23.3.4.7", + "22.3.4.21" + ], + "rebar3_version": [ + "3.16.1" + ], + "os": [ + "ubuntu-18.04" + ], + "include": [ + { + "otp_version": "21.3.8.24", + "rebar3_version": "3.15.2" + } + ] +} diff --git a/.github/labeler.yml b/.github/labeler.yml index 0c46bbe..edb13e3 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -15,16 +15,16 @@ elixir: erlang: - instrumentation/**/*.erl - instrumentation/**/*.hrl - - instrumentation/**/rebar.config + - instrumentation/**/rebar.* - propagators/**/*.erl - propagators/**/*.hrl - - propagators/**/rebar.config + - propagators/**/rebar.* - exporters/**/*.erl - exporters/**/*.hrl - - exporters/**/rebar.config + - exporters/**/rebar.* - examples/**/*.erl - examples/**/*.hrl - - examples/**/rebar.config + - examples/**/rebar.* instrumentation: - instrumentation/**/* @@ -38,5 +38,8 @@ examples: scope-ci: - .github/workflows/** +opentelemetry_cowboy: + - instrumentation/opentelemetry_cowboy/**/* + opentelemetry_phoenix: - instrumentation/opentelemetry_phoenix/**/* diff --git a/.github/workflows/erlang.yml b/.github/workflows/erlang.yml new file mode 100644 index 0000000..4379164 --- /dev/null +++ b/.github/workflows/erlang.yml @@ -0,0 +1,53 @@ +name: Erlang + +on: + pull_request: + branches: + - 'main' + types: [opened, reopened, synchronize, labeled] + push: + branches: + - 'main' + +jobs: + test-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v2 + - name: Read file + id: set-matrix + run: | + matrixStringifiedObject="{\"otp_version\":[\"24.0.6\",\"23.3.4.7\",\"22.3.4.21\"],\"rebar3_version\":[\"3.16.1\"],\"os\":[\"ubuntu-18.04\"],\"include\":[{\"otp_version\":\"21.3.8.24\",\"rebar3_version\":\"3.15.2\"}]}" + echo "::set-output name=matrix::$matrixStringifiedObject" + opentelemetry-cowboy: + needs: [test-matrix] + if: (contains(github.event.pull_request.labels.*.name, 'erlang') && contains(github.event.pull_request.labels.*.name, 'opentelemetry_cowboy')) + env: + app: 'opentelemetry_cowboy' + defaults: + run: + working-directory: instrumentation/${{ env.app }} + runs-on: ubuntu-18.04 + name: Opentelemetry Cowboy test on OTP ${{ matrix.otp_version }} with Rebar3 ${{ matrix.rebar3_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 }} + rebar3-version: ${{ matrix.rebar3_version }} + - name: Cache + uses: actions/cache@v2 + with: + path: | + ${{ env.app }}/_build + key: ${{ runner.os }}-build-${{ matrix.otp_version }}-${{ matrix.rebar3_version }}-v3-${{ hashFiles(format('{0}{1}', github.workspace, 'instrumentation/${{ env.app }}/rebar.lock')) }} + - name: Fetch deps + if: steps.deps-cache.outputs.cache-hit != 'true' + run: rebar3 get-deps + - name: Test + run: rebar3 ct diff --git a/CODEOWNERS b/CODEOWNERS index 982216a..51347ed 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -14,4 +14,5 @@ @open-telemetry/erlang-approvers -/instrumentation/opentelemetry_phoenix @bryannaegele @tristansloughter \ No newline at end of file +/instrumentation/opentelemetry_cowboy @bryannaegele @tsloughter +/instrumentation/opentelemetry_phoenix @bryannaegele @tsloughter \ No newline at end of file diff --git a/README.md b/README.md index 925dc96..0ff0819 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,7 @@ core distribution of the API and SDK. OpenTelemetry can collect tracing data using instrumentation. Vendors/Users can also create and use their own. Currently, OpenTelemetry supports automatic tracing for: -### Elixir Instrumentation - +- [opentelemetry-cowboy](https://github.com/open-telemetry/opentelemetry-erlang-contrib/tree/main/instrumentation/opentelemetry_cowboy) - [opentelemetry-phoenix](https://github.com/open-telemetry/opentelemetry-erlang-contrib/tree/main/instrumentation/opentelemetry_phoenix) ## Supported Runtimes diff --git a/instrumentation/opentelemetry_cowboy/.gitignore b/instrumentation/opentelemetry_cowboy/.gitignore new file mode 100644 index 0000000..f1c4554 --- /dev/null +++ b/instrumentation/opentelemetry_cowboy/.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/instrumentation/opentelemetry_cowboy/LICENSE b/instrumentation/opentelemetry_cowboy/LICENSE new file mode 100644 index 0000000..d665b3c --- /dev/null +++ b/instrumentation/opentelemetry_cowboy/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/instrumentation/opentelemetry_cowboy/README.md b/instrumentation/opentelemetry_cowboy/README.md new file mode 100644 index 0000000..e524c1a --- /dev/null +++ b/instrumentation/opentelemetry_cowboy/README.md @@ -0,0 +1,33 @@ +# opentelemetry_cowboy + +[![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_phoenix)](https://hex.pm/packages/opentelemetry_cowboy) +![Build Status](https://github.com/open-telemetry/opentelemetry-erlang-contrib/workflows/Erlang/badge.svg) + +Telemetry handler that creates Opentelemetry spans from Phoenix events. + +After installing, setup the handler in your application behaviour before your +top-level supervisor starts. + +```erlang +opentelemetry_cowboy:setup() +``` + +See [cowboy_telemetry](https://github.com/beam-telemetry/cowboy_telemetry) for prerequisite setup. + +There is no additional prerequisite setup for [plug_cowboy](https://hex.pm/packages/plug_cowboy) users. + +## Installation + +```erlang +{deps, [ + {opentelemetry_cowboy, "~> 1.0.0-beta"} +]} +``` +```elixir +def deps do + [ + {:opentelemetry_cowboy, "~> 1.0.0-beta"} + ] +end +``` diff --git a/instrumentation/opentelemetry_cowboy/rebar.config b/instrumentation/opentelemetry_cowboy/rebar.config new file mode 100644 index 0000000..615ad31 --- /dev/null +++ b/instrumentation/opentelemetry_cowboy/rebar.config @@ -0,0 +1,32 @@ +{erl_opts, [debug_info]}. +{deps, [ + {opentelemetry_api, "~> 1.0.0-rc"}, + {opentelemetry_telemetry, "~> 1.0.0-beta"}, + {telemetry, "~> 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.0-rc"}, + {cowboy, "~> 2.7"}, + {cowboy_telemetry, "~> 0.3"} + ]}, + {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"]}]}. \ No newline at end of file diff --git a/instrumentation/opentelemetry_cowboy/rebar.lock b/instrumentation/opentelemetry_cowboy/rebar.lock new file mode 100644 index 0000000..f064bc6 --- /dev/null +++ b/instrumentation/opentelemetry_cowboy/rebar.lock @@ -0,0 +1,19 @@ +{"1.2.0", +[{<<"opentelemetry_api">>,{pkg,<<"opentelemetry_api">>,<<"1.0.0-rc.2">>},0}, + {<<"opentelemetry_telemetry">>, + {pkg,<<"opentelemetry_telemetry">>,<<"1.0.0-beta.2">>}, + 0}, + {<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.0.0">>},0}, + {<<"telemetry_registry">>,{pkg,<<"telemetry_registry">>,<<"0.3.0">>},0}]}. +[ +{pkg_hash,[ + {<<"opentelemetry_api">>, <<"A0EC5B242BB7CE7563B4891E77DCFA529DEFC9E42C19A5A702574C5AC3D0C6E7">>}, + {<<"opentelemetry_telemetry">>, <<"B840EEE9E68307AD7FA4EE316DA19DB3F8E30763B87737D3304782CA3CC296A2">>}, + {<<"telemetry">>, <<"0F453A102CDF13D506B7C0AB158324C337C41F1CC7548F0BC0E130BBF0AE9452">>}, + {<<"telemetry_registry">>, <<"6768F151EA53FC0FBCA70DBFF5B20A8D663EE4E0C0B2AE589590E08658E76F1E">>}]}, +{pkg_hash_ext,[ + {<<"opentelemetry_api">>, <<"426A969C8EE2AFA8AB55B58E6E40E81C1F934C064459A1ACB530F54042F9A9A3">>}, + {<<"opentelemetry_telemetry">>, <<"E8B12F42614D0AEB6A49001C75CA035544950F736FDBB240177838674F99E1E2">>}, + {<<"telemetry">>, <<"73BC09FA59B4A0284EFB4624335583C528E07EC9AE76ACA96EA0673850AEC57A">>}, + {<<"telemetry_registry">>, <<"492E2ADBC609F3E79ECE7F29FEC363A97A2C484AC78A83098535D6564781E917">>}]} +]. diff --git a/instrumentation/opentelemetry_cowboy/src/opentelemetry_cowboy.app.src b/instrumentation/opentelemetry_cowboy/src/opentelemetry_cowboy.app.src new file mode 100644 index 0000000..5674e1a --- /dev/null +++ b/instrumentation/opentelemetry_cowboy/src/opentelemetry_cowboy.app.src @@ -0,0 +1,17 @@ +{application, opentelemetry_cowboy, + [{description, "An OTP library"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, + [kernel, + stdlib, + opentelemetry_api, + telemetry, + telemetry_registry + ]}, + {env,[]}, + {modules, []}, + + {licenses, ["Apache 2.0"]}, + {links, []} + ]}. diff --git a/instrumentation/opentelemetry_cowboy/src/opentelemetry_cowboy.erl b/instrumentation/opentelemetry_cowboy/src/opentelemetry_cowboy.erl new file mode 100644 index 0000000..24facc9 --- /dev/null +++ b/instrumentation/opentelemetry_cowboy/src/opentelemetry_cowboy.erl @@ -0,0 +1,125 @@ +-module(opentelemetry_cowboy). + +-export([ + setup/0, + setup/1, + handle_event/4]). + +-include_lib("opentelemetry_api/include/opentelemetry.hrl"). + +-define(TRACER_ID, opentelemetry_cowboy). + +-spec setup() -> ok. +setup() -> + setup([]). + +-spec setup([]) -> ok. +setup(_Opts) -> + register_tracer(), + attach_event_handlers(), + ok. + +register_tracer() -> + opentelemetry:register_tracer(?MODULE, "0.1.0"). + +attach_event_handlers() -> + Events = [ + [cowboy, request, early_error], + [cowboy, request, start], + [cowboy, request, stop], + [cowboy, request, exception] + ], + telemetry:attach_many(opentelemetry_cowboy_handlers, Events, fun ?MODULE:handle_event/4, #{}). + +handle_event([cowboy, request, start], _Measurements, #{req := Req} = Meta, _Config) -> + Headers = maps:get(headers, Req), + otel_propagator:text_map_extract(maps:to_list(Headers)), + {RemoteIP, _Port} = maps:get(peer, Req), + Method = maps:get(method, Req), + + Attributes = [ + {'http.client_ip', client_ip(Headers, RemoteIP)}, + {'http.flavor', http_flavor(Req)}, + {'http.host', maps:get(host, Req)}, + {'http.host.port', maps:get(port, Req)}, + {'http.method', Method}, + {'http.scheme', maps:get(scheme, Req)}, + {'http.target', maps:get(path, Req)}, + {'http.user_agent', maps:get(<<"user-agent">>, Headers, <<"">>)}, + {'net.host.ip', iolist_to_binary(inet:ntoa(RemoteIP))}, + {'net.transport', 'IP.TCP'} + ], + SpanName = iolist_to_binary([<<"HTTP ">>, Method]), + Ctx = otel_telemetry:start_telemetry_span(?TRACER_ID, SpanName, Meta, #{}), + otel_span:set_attributes(Ctx, Attributes); + +handle_event([cowboy, request, stop], Measurements, Meta, _Config) -> + Ctx = otel_telemetry:set_current_telemetry_span(?TRACER_ID, Meta), + Status = maps:get(resp_status, Meta), + Attributes = [ + {'http.request_content_length', maps:get(req_body_length, Measurements)}, + {'http.response_content_length', maps:get(resp_body_length, Measurements)} + ], + otel_span:set_attributes(Ctx, Attributes), + case Status of + undefined -> + {ErrorType, Error, Reason} = maps:get(error, Meta), + otel_span:add_event(Ctx, atom_to_binary(ErrorType, utf8), [{error, Error}, {reason, Reason}]), + otel_span:set_status(Ctx, opentelemetry:status(?OTEL_STATUS_ERROR, Reason)); + Status when Status >= 400 -> + otel_span:set_attributes(Ctx, [{'http.status', Status}]), + otel_span:set_status(Ctx, opentelemetry:status(?OTEL_STATUS_ERROR, <<"">>)); + Status when Status < 400 -> + otel_span:set_attributes(Ctx, [{'http.status', Status}]) + end, + otel_telemetry:end_telemetry_span(?TRACER_ID, Meta); + +handle_event([cowboy, request, exception], Measurements, Meta, _Config) -> + Ctx = otel_telemetry:set_current_telemetry_span(?TRACER_ID, Meta), + #{ + kind := Kind, + reason := Reason, + stacktrace := Stacktrace, + resp_status := Status + } = Meta, + otel_span:record_exception(Ctx, Kind, Reason, Stacktrace, []), + otel_span:set_status(Ctx, opentelemetry:status(?OTEL_STATUS_ERROR, <<"">>)), + otel_span:set_attributes(Ctx, [ + {'http.status', Status}, + {'http.request_content_length', maps:get(req_body_length, Measurements)}, + {'http.response_content_length', maps:get(resp_body_length, Measurements)} + ]), + otel_telemetry:end_telemetry_span(?TRACER_ID, Meta); + +handle_event([cowboy, request, early_error], Measurements, Meta, _Config) -> + Ctx = otel_telemetry:start_telemetry_span(?TRACER_ID, <<"HTTP Error">>, Meta, #{}), + #{ + reason := {ErrorType, Error, Reason}, + resp_status := Status + } = Meta, + + otel_span:set_attributes(Ctx, [ + {'http.status', Status}, + {'http.response_content_length', maps:get(resp_body_length, Measurements)} + ]), + otel_span:add_event(Ctx, atom_to_binary(ErrorType, utf8), [{error, Error}, {reason, Reason}]), + otel_span:set_status(Ctx, opentelemetry:status(?OTEL_STATUS_ERROR, Reason)), + otel_telemetry:end_telemetry_span(?TRACER_ID, Meta). + +http_flavor(Req) -> + case maps:get(version, Req, undefined) of + 'HTTP/1.0' -> '1.0'; + 'HTTP/1.1' -> '1.1'; + 'HTTP/2' -> '2.0'; + 'SPDY' -> 'SPDY'; + 'QUIC' -> 'QUIC'; + _ -> <<"">> + end. + +client_ip(Headers, RemoteIP) -> + case maps:get(<<"x-forwarded-for">>, Headers, undefined) of + undefined -> + iolist_to_binary(inet:ntoa(RemoteIP)); + Addresses -> + hd(binary:split(Addresses, <<",">>)) + end. diff --git a/instrumentation/opentelemetry_cowboy/test/opentelemetry_cowboy_SUITE.erl b/instrumentation/opentelemetry_cowboy/test/opentelemetry_cowboy_SUITE.erl new file mode 100644 index 0000000..11d7c9a --- /dev/null +++ b/instrumentation/opentelemetry_cowboy/test/opentelemetry_cowboy_SUITE.erl @@ -0,0 +1,255 @@ +-module(opentelemetry_cowboy_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). +-include_lib("opentelemetry_api/include/opentelemetry.hrl"). +-include_lib("opentelemetry/include/otel_span.hrl"). +-include_lib("opentelemetry_api/include/otel_tracer.hrl"). + +-define(assertListsMatch(List1, List2), ?assertEqual(lists:sort(List1), lists:sort(List2))). + +all() -> + [ + successful_request, + chunked_request, + failed_request, + client_timeout_request, + idle_timeout_request, + chunk_timeout_request, + bad_request + ]. + +init_per_suite(Config) -> + ok = application:load(opentelemetry), + {ok,_} = application:ensure_all_started(ranch), + Dispatch = cowboy_router:compile([{"localhost", [ + {"/success", test_h, success}, + {"/chunked", test_h, chunked}, + {"/chunked_slow", test_h, chunked_slow}, + {"/slow", test_h, slow}, + {"/failure", test_h, failure} + ]}]), + {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ + env => #{dispatch => Dispatch}, + stream_handlers => [cowboy_telemetry_h, cowboy_stream_h], + idle_timeout => 150 + } + ), + Config. + +end_per_suite(_Config) -> + application:unload(opentelemetry), + application:stop(ranch), + application:stop(telemetry). + +init_per_testcase(_, Config) -> + application:set_env(opentelemetry, processors, [{otel_batch_processor, #{scheduled_delay_ms => 1}}]), + + {ok, _} = application:ensure_all_started(opentelemetry), + {ok, _} = application:ensure_all_started(opentelemetry_telemetry), + opentelemetry_cowboy:setup(), + + otel_batch_processor:set_exporter(otel_exporter_pid, self()), + + Config. + +end_per_testcase(_, Config) -> + application:stop(telemetry), + application:stop(opentelemetry_telemetry), + application:stop(opentelemetry), + + Config. + +successful_request(_Config) -> + Headers = [ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"}, + {"tracestate", "congo=t61rcWkgMzE"}, + {"x-forwarded-for", "203.0.133.195, 70.41.3.18, 150.172.238.178"}, + {"user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:81.0) Gecko/20100101 Firefox/81.0"}], + {ok, {{_Version, 200, _ReasonPhrase}, _Headers, _Body}} = + httpc:request(get, {"http://localhost:8080/success", Headers}, [], []), + receive + {span, #span{name=Name,events=[],attributes=Attributes,parent_span_id=ParentSpanId}} -> + ?assertEqual(<<"HTTP GET">>, Name), + ?assertEqual(13235353014750950193, ParentSpanId), + ExpectedAttrs = [ + {'http.client_ip', <<"203.0.133.195">>}, + {'http.flavor', '1.1'}, + {'http.host', <<"localhost">>}, + {'http.host.port', 8080}, + {'http.method', <<"GET">>}, + {'http.scheme', <<"http">>}, + {'http.target', <<"/success">>}, + {'http.user_agent', <<"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:81.0) Gecko/20100101 Firefox/81.0">>}, + {'net.host.ip', <<"127.0.0.1">>}, + {'net.transport', 'IP.TCP'}, + {'http.status', 200}, + {'http.request_content_length', 0}, + {'http.response_content_length', 12}], + ?assertListsMatch(ExpectedAttrs, Attributes) + after + 1000 -> ct:fail(successful_request) + end. + +chunked_request(_Config) -> + {ok, {{_Version, 200, _ReasonPhrase}, _Headers, _Body}} = + httpc:request(get, {"http://localhost:8080/chunked", []}, [], []), + receive + {span, #span{name=Name,events=[],attributes=Attributes,parent_span_id=undefined}} -> + ?assertEqual(<<"HTTP GET">>, Name), + ExpectedAttrs = [ + {'http.client_ip', <<"127.0.0.1">>}, + {'http.flavor', '1.1'}, + {'http.host', <<"localhost">>}, + {'http.host.port', 8080}, + {'http.method', <<"GET">>}, + {'http.scheme', <<"http">>}, + {'http.target', <<"/chunked">>}, + {'http.user_agent', <<>>}, + {'net.host.ip', <<"127.0.0.1">>}, + {'net.transport', 'IP.TCP'}, + {'http.status', 200}, + {'http.request_content_length', 0}, + {'http.response_content_length', 14}], + ?assertListsMatch(ExpectedAttrs, Attributes) + after + 1000 -> ct:fail(chunked_request) + end. + +failed_request(_Config) -> + {ok, {{_Version, 500, _ReasonPhrase}, _Headers, _Body}} = + httpc:request(get, {"http://localhost:8080/failure", []}, [], []), + receive + {span, #span{name=Name,events=[Event],attributes=Attributes,parent_span_id=undefined}} -> + { + event,_,<<"exception">>,[ + {<<"exception.type">>, <<"exit:failure">>}, + {<<"exception.stacktrace">>, _Stacktrace}] + } = Event, + ?assertEqual(<<"HTTP GET">>, Name), + ExpectedAttrs = [ + {'http.client_ip', <<"127.0.0.1">>}, + {'http.flavor', '1.1'}, + {'http.host', <<"localhost">>}, + {'http.host.port', 8080}, + {'http.method', <<"GET">>}, + {'http.scheme', <<"http">>}, + {'http.target', <<"/failure">>}, + {'http.user_agent', <<>>}, + {'net.host.ip', <<"127.0.0.1">>}, + {'net.transport', 'IP.TCP'}, + {'http.status', 500}, + {'http.request_content_length', 0}, + {'http.response_content_length', 0}], + ?assertListsMatch(ExpectedAttrs, Attributes) + after + 1000 -> ct:fail(failed_request) + end. + +client_timeout_request(_Config) -> + {error, timeout} = + httpc:request(get, {"http://localhost:8080/slow", []}, [{timeout, 50}], []), + receive + {span, #span{name=Name,events=[Event],attributes=Attributes,parent_span_id=undefined}} -> + { + event,_,<<"socket_error">>,[ + {error, closed}, + {reason, 'The socket has been closed.'}] + } = Event, + ?assertEqual(<<"HTTP GET">>, Name), + ExpectedAttrs = [ + {'http.client_ip', <<"127.0.0.1">>}, + {'http.flavor', '1.1'}, + {'http.host', <<"localhost">>}, + {'http.host.port', 8080}, + {'http.method', <<"GET">>}, + {'http.scheme', <<"http">>}, + {'http.target', <<"/slow">>}, + {'http.user_agent', <<>>}, + {'net.host.ip', <<"127.0.0.1">>}, + {'net.transport', 'IP.TCP'}, + {'http.request_content_length', 0}, + {'http.response_content_length', 0}], + ?assertListsMatch(ExpectedAttrs, Attributes) + after + 1000 -> ct:fail(client_timeout_request) + end. + +idle_timeout_request(_Config) -> + {error, socket_closed_remotely} = + httpc:request(head, {"http://localhost:8080/slow", []}, [], []), + receive + {span, #span{name=Name,events=[Event],attributes=Attributes,parent_span_id=undefined}} -> + { + event,_,<<"connection_error">>,[ + {error, timeout}, + {reason, 'Connection idle longer than configuration allows.'}] + } = Event, + ?assertEqual(<<"HTTP HEAD">>, Name), + ExpectedAttrs = [ + {'http.client_ip', <<"127.0.0.1">>}, + {'http.flavor', '1.1'}, + {'http.host', <<"localhost">>}, + {'http.host.port', 8080}, + {'http.method', <<"HEAD">>}, + {'http.scheme', <<"http">>}, + {'http.target', <<"/slow">>}, + {'http.user_agent', <<>>}, + {'net.host.ip', <<"127.0.0.1">>}, + {'net.transport', 'IP.TCP'}, + {'http.request_content_length', 0}, + {'http.response_content_length', 0}], + ?assertListsMatch(ExpectedAttrs, Attributes) + after + 1000 -> ct:fail(idle_timeout_request) + end. + +chunk_timeout_request(_Config) -> + httpc:request(head, {"http://localhost:8080/chunked_slow", []}, [], []), + receive + {span, #span{name=Name,events=[],attributes=Attributes,parent_span_id=undefined}} -> + ?assertEqual(<<"HTTP HEAD">>, Name), + ExpectedAttrs = [ + {'http.client_ip', <<"127.0.0.1">>}, + {'http.flavor', '1.1'}, + {'http.host', <<"localhost">>}, + {'http.host.port', 8080}, + {'http.method', <<"HEAD">>}, + {'http.scheme', <<"http">>}, + {'http.target', <<"/chunked_slow">>}, + {'http.user_agent', <<>>}, + {'net.host.ip', <<"127.0.0.1">>}, + {'net.transport', 'IP.TCP'}, + {'http.status',200}, + {'http.request_content_length', 0}, + {'http.response_content_length', 0}], + ?assertListsMatch(ExpectedAttrs, Attributes) + after + 1000 -> ct:fail(chunk_timeout_request) + end. + +bad_request(_Config) -> + Headers = [ + {"traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"}, + {"tracestate", "congo=t61rcWkgMzE"}, + {"x-forwarded-for", "203.0.133.195, 70.41.3.18, 150.172.238.178"}], + {ok, {{_Version, 501, _ReasonPhrase}, _Headers, _Body}} = + httpc:request(trace, {"http://localhost:8080/", Headers}, [], []), + receive + {span, #span{name=Name,events=[Event],attributes=Attributes,parent_span_id=undefined}} -> + { + event,_,<<"connection_error">>,[ + {error, no_error}, + {reason, 'The TRACE method is currently not implemented. (RFC7231 4.3.8)'}] + } = Event, + ?assertEqual(<<"HTTP Error">>, Name), + ExpectedAttrs = [ + {'http.status', 501}, + {'http.response_content_length', 0}], + ?assertListsMatch(ExpectedAttrs, Attributes) + after + 1000 -> ct:fail(bad_request) + end. diff --git a/instrumentation/opentelemetry_cowboy/test/test_h.erl b/instrumentation/opentelemetry_cowboy/test/test_h.erl new file mode 100644 index 0000000..d6d142c --- /dev/null +++ b/instrumentation/opentelemetry_cowboy/test/test_h.erl @@ -0,0 +1,23 @@ +-module(test_h). +-behaviour(cowboy_handler). + +-export([init/2]). + +init(_, failure) -> + error(failure); +init(Req, success = Opts) -> + {ok, cowboy_req:reply(200, #{}, <<"Hello world!">>, Req), Opts}; +init(Req, slow = Opts) -> + timer:sleep(200), + {ok, cowboy_req:reply(200, #{}, <<"I'm slow">>, Req), Opts}; +init(Req0, chunked = Opts) -> + Req = cowboy_req:stream_reply(200, Req0), + cowboy_req:stream_body("Hello\r\n", nofin, Req), + cowboy_req:stream_body("World\r\n", fin, Req), + {ok, Req, Opts}; +init(Req0, chunked_slow = Opts) -> + Req = cowboy_req:stream_reply(200, Req0), + cowboy_req:stream_body("Hello\r\n", nofin, Req), + timer:sleep(200), + cowboy_req:stream_body("World\r\n", fin, Req), + {ok, Req, Opts}.