├── .formatter.exs ├── .git-blame-ignore-revs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── elixir.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── VERSION ├── config ├── config.exs └── test.exs ├── examples ├── .formatter.exs ├── .gitignore ├── README.md ├── apps │ ├── absinthe_example │ │ ├── .formatter.exs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── config │ │ │ └── config.exs │ │ ├── lib │ │ │ ├── application.ex │ │ │ ├── resolvers.ex │ │ │ ├── router.ex │ │ │ └── schema.ex │ │ ├── mix.exs │ │ └── test │ │ │ ├── absinthe_example_test.exs │ │ │ └── test_helper.exs │ ├── ecto_example │ │ ├── .formatter.exs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── config │ │ │ └── config.exs │ │ ├── lib │ │ │ └── ecto_example │ │ │ │ ├── application.ex │ │ │ │ ├── count.ex │ │ │ │ ├── database.ex │ │ │ │ ├── migration.ex │ │ │ │ ├── mysql_repo.ex │ │ │ │ ├── postgres_repo.ex │ │ │ │ ├── router.ex │ │ │ │ └── sqlite3_repo.ex │ │ ├── mix.exs │ │ └── test │ │ │ ├── ecto_example_test.exs │ │ │ └── test_helper.exs │ ├── instrumented_task │ │ ├── .formatter.exs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── lib │ │ │ ├── instrumented_task.ex │ │ │ └── uninstrumented_task.ex │ │ └── mix.exs │ ├── oban_example │ │ ├── .formatter.exs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── lib │ │ │ └── oban_example │ │ │ │ ├── application.ex │ │ │ │ └── worker.ex │ │ ├── mix.exs │ │ └── test │ │ │ ├── oban_example_test.exs │ │ │ └── test_helper.exs │ ├── phx_example │ │ ├── .formatter.exs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── assets │ │ │ └── js │ │ │ │ └── app.js │ │ ├── config │ │ │ └── config.exs │ │ ├── lib │ │ │ ├── phx_example │ │ │ │ └── application.ex │ │ │ ├── phx_example_web.ex │ │ │ └── phx_example_web │ │ │ │ ├── bandit_endpoint.ex │ │ │ │ ├── components │ │ │ │ ├── layouts.ex │ │ │ │ └── layouts │ │ │ │ │ ├── app.html.heex │ │ │ │ │ └── root.html.heex │ │ │ │ ├── controllers │ │ │ │ ├── error_html.ex │ │ │ │ ├── error_html │ │ │ │ │ └── 500.html.heex │ │ │ │ ├── page_controller.ex │ │ │ │ ├── page_html.ex │ │ │ │ └── page_html │ │ │ │ │ └── index.html.heex │ │ │ │ ├── endpoint.ex │ │ │ │ ├── live │ │ │ │ ├── error_live.ex │ │ │ │ └── home_live.ex │ │ │ │ └── router.ex │ │ ├── mix.exs │ │ └── test │ │ │ ├── phx_example_test.exs │ │ │ └── test_helper.exs │ ├── redix_example │ │ ├── .formatter.exs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── config │ │ │ └── config.exs │ │ ├── lib │ │ │ └── redix_example │ │ │ │ ├── application.ex │ │ │ │ └── router.ex │ │ ├── mix.exs │ │ └── test │ │ │ ├── redix_example_test.exs │ │ │ └── test_helper.exs │ └── w3c_trace_context_validation │ │ ├── .formatter.exs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── config │ │ └── config.exs │ │ ├── lib │ │ ├── application.ex │ │ └── router.ex │ │ ├── mix.exs │ │ └── test │ │ ├── test_helper.exs │ │ └── w3c_trace_context_validation_test.exs ├── config │ └── config.exs ├── docker-compose.yml ├── mix.exs └── mix.lock ├── lib ├── new_relic.ex └── new_relic │ ├── aggregate │ ├── aggregate.ex │ ├── reporter.ex │ └── supervisor.ex │ ├── always_on_supervisor.ex │ ├── application.ex │ ├── config.ex │ ├── custom │ └── event.ex │ ├── distributed_trace.ex │ ├── distributed_trace │ ├── backoff_sampler.ex │ ├── context.ex │ ├── new_relic_context.ex │ ├── supervisor.ex │ ├── w3c_trace_context.ex │ └── w3c_trace_context │ │ ├── trace_parent.ex │ │ └── trace_state.ex │ ├── enabled_supervisor.ex │ ├── enabled_supervisor_manager.ex │ ├── error │ ├── event.ex │ ├── logger_filter.ex │ ├── reporter │ │ ├── crash_report.ex │ │ └── error_msg.ex │ ├── supervisor.ex │ └── trace.ex │ ├── error_logger.ex │ ├── graceful_shutdown.ex │ ├── harvest │ ├── collector │ │ ├── agent_run.ex │ │ ├── connect.ex │ │ ├── custom_event │ │ │ └── harvester.ex │ │ ├── error_trace │ │ │ └── harvester.ex │ │ ├── metric │ │ │ └── harvester.ex │ │ ├── protocol.ex │ │ ├── span_event │ │ │ └── harvester.ex │ │ ├── supervisor.ex │ │ ├── transaction_error_event │ │ │ └── harvester.ex │ │ ├── transaction_event │ │ │ └── harvester.ex │ │ └── transaction_trace │ │ │ └── harvester.ex │ ├── data_supervisor.ex │ ├── harvest_cycle.ex │ ├── harvester_store.ex │ ├── harvester_supervisor.ex │ ├── supervisor.ex │ └── telemetry_sdk │ │ ├── api.ex │ │ ├── config.ex │ │ ├── dimensional_metrics │ │ └── harvester.ex │ │ ├── logs │ │ └── harvester.ex │ │ ├── spans │ │ └── harvester.ex │ │ └── supervisor.ex │ ├── init.ex │ ├── instrumented │ ├── mix │ │ └── task.ex │ ├── task.ex │ └── task │ │ ├── supervisor.ex │ │ └── wrappers.ex │ ├── json.ex │ ├── logger.ex │ ├── logs_in_context.ex │ ├── logs_in_context │ └── supervisor.ex │ ├── metric │ ├── metric.ex │ └── metric_data.ex │ ├── os_mon.ex │ ├── other_transaction.ex │ ├── sampler │ ├── agent.ex │ ├── beam.ex │ ├── ets.ex │ ├── process.ex │ ├── reporter.ex │ ├── supervisor.ex │ └── top_process.ex │ ├── signal_handler.ex │ ├── span │ ├── event.ex │ └── reporter.ex │ ├── telemetry │ ├── absinthe.ex │ ├── ecto.ex │ ├── ecto │ │ ├── handler.ex │ │ ├── metadata.ex │ │ └── supervisor.ex │ ├── finch.ex │ ├── oban.ex │ ├── phoenix.ex │ ├── phoenix_live_view.ex │ ├── plug.ex │ ├── redix.ex │ └── supervisor.ex │ ├── tracer.ex │ ├── tracer │ ├── direct.ex │ ├── macro.ex │ └── report.ex │ ├── transaction.ex │ ├── transaction │ ├── complete.ex │ ├── erlang_trace.ex │ ├── erlang_trace_manager.ex │ ├── erlang_trace_supervisor.ex │ ├── event.ex │ ├── reporter.ex │ ├── sidecar.ex │ ├── sidecar_store.ex │ ├── supervisor.ex │ └── trace.ex │ ├── util.ex │ └── util │ ├── apdex.ex │ ├── conditional_compile.ex │ ├── error.ex │ ├── event.ex │ ├── http.ex │ ├── priority_queue.ex │ ├── request_start.ex │ └── vendor.ex ├── mix.exs ├── mix.lock └── test ├── aggregate_test.exs ├── backoff_sampler_test.exs ├── collector_protocol_test.exs ├── config_test.exs ├── custom_event_test.exs ├── dimensional_metric_test.exs ├── distributed_trace_test.exs ├── erlang_trace_overload_test.exs ├── erlang_trace_test.exs ├── error_test.exs ├── error_trace_test.exs ├── evil_collector_test.exs ├── infinite_tracing_test.exs ├── init_test.exs ├── instrumented └── task_test.exs ├── integration └── integration_test.exs ├── logger_test.exs ├── logs_in_context_test.exs ├── metric_error_test.exs ├── metric_harvester_test.exs ├── metric_test.exs ├── metric_tracer_test.exs ├── metric_transaction_test.exs ├── other_transaction_test.exs ├── priority_queue_test.exs ├── request_queue_time_test.exs ├── sampler_test.exs ├── sidecar_test.exs ├── span_event_test.exs ├── ssl_test.exs ├── support └── test_helper.ex ├── telemetry ├── ecto_test.exs └── finch_test.exs ├── telemetry_sdk ├── config_test.exs ├── dimensional_metrics_harvester_test.exs ├── logs_harvester_test.exs └── span_harvester_test.exs ├── test_helper.exs ├── tracer_macro_test.exs ├── tracer_test.exs ├── transaction_error_event_test.exs ├── transaction_event_test.exs ├── transaction_test.exs ├── transaction_trace_test.exs ├── util_test.exs └── wc3_trace_context_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | line_length: 120, 3 | inputs: [ 4 | "{lib,config,test}/**/*.{ex,exs}", 5 | "{mix,.formatter}.exs" 6 | ], 7 | subdirectories: [ 8 | "examples/apps/*" 9 | ] 10 | ] 11 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Change public functions to be private. 2 | 4b24a127a02d8d641c0f72f0f8538a5daca0d125 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **Environment** 11 | * Elixir & Erlang version (`elixir -v`): 12 | * Agent version (`mix deps | grep new_relic_agent`): 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 16 | 17 | **Is your feature request related to a problem? Please describe.** 18 | A clear and concise description of what the problem is. 19 | 20 | **Describe the solution you'd like** 21 | A clear and concise description of what you want to happen. 22 | 23 | **Describe alternatives you've considered** 24 | A clear and concise description of any alternative solutions or features you've considered. 25 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /doc 4 | erl_crash.dump 5 | *.ez 6 | tmp 7 | config/secret.exs 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at newrelic-elixir-agent@googlegroups.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.38.0 2 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if Mix.env() == :test do 4 | import_config("test.exs") 5 | else 6 | if File.exists?("config/secret.exs"), 7 | do: import_config("secret.exs") 8 | end 9 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :warning 4 | 5 | config :new_relic_agent, 6 | app_name: "ElixirAgentTest", 7 | license_key: "license_key", 8 | bypass_collector: true, 9 | automatic_attributes: [test_attribute: "test_value"], 10 | ignore_paths: ["/ignore/this", ~r(ignore/these/*.)], 11 | log: "Logger" 12 | -------------------------------------------------------------------------------- /examples/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "config/*.exs"], 4 | subdirectories: ["apps/*"] 5 | ] 6 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | /tmp/ 23 | 24 | # Secrets file for local development 25 | secret.exs 26 | 27 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Example applications 2 | 3 | A set example apps demonstrating and validating built-in instrumentation. 4 | 5 | ### Running tests 6 | 7 | In CI, these test are always run. When developing locally, you can run the tests easily by first starting the dependent docker services: 8 | 9 | ``` 10 | docker compose up 11 | mix test 12 | ``` 13 | 14 | ### Adding example apps 15 | 16 | Create the app 17 | 18 | ``` 19 | cd apps 20 | mix new --sup example 21 | ``` 22 | 23 | Point to the agent 24 | 25 | ```elixir 26 | defp deps do 27 | [ 28 | {:new_relic_agent, path: "../../../"}, 29 | # ... 30 | ] 31 | end 32 | ``` 33 | -------------------------------------------------------------------------------- /examples/apps/absinthe_example/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | import_deps: [:absinthe, :plug] 5 | ] 6 | -------------------------------------------------------------------------------- /examples/apps/absinthe_example/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | absinthe_example-*.tar 24 | 25 | 26 | # Temporary files for e.g. tests 27 | /tmp 28 | -------------------------------------------------------------------------------- /examples/apps/absinthe_example/README.md: -------------------------------------------------------------------------------- 1 | # Absinthe 2 | 3 | An example of an `Absinthe` GraphQL API instrumented by the New Relic Agent via `telemetry`. 4 | 5 | This instrumentation is fully automatic. 6 | -------------------------------------------------------------------------------- /examples/apps/absinthe_example/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :absinthe_example, 4 | http_port: 4006 5 | -------------------------------------------------------------------------------- /examples/apps/absinthe_example/lib/application.ex: -------------------------------------------------------------------------------- 1 | defmodule AbsintheExample.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_type, _args) do 6 | http_port = Application.get_env(:absinthe_example, :http_port) 7 | 8 | children = [ 9 | Plug.Cowboy.child_spec( 10 | scheme: :http, 11 | plug: AbsintheExample.Router, 12 | options: [port: http_port] 13 | ) 14 | ] 15 | 16 | opts = [strategy: :one_for_one, name: AbsintheExample.Supervisor] 17 | Supervisor.start_link(children, opts) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/apps/absinthe_example/lib/resolvers.ex: -------------------------------------------------------------------------------- 1 | defmodule AbsintheExample.Resolvers do 2 | use NewRelic.Tracer 3 | 4 | def echo(_source, %{this: this}, _res) do 5 | {:ok, do_echo(this)} 6 | end 7 | 8 | @trace :do_echo 9 | defp do_echo(this), do: this 10 | 11 | def one(_source, _args, _res) do 12 | Process.sleep(1) 13 | {:ok, %{two: %{}}} 14 | end 15 | 16 | def three(_source, %{value: value}, _res) do 17 | Process.sleep(2) 18 | {:ok, do_three(value)} 19 | end 20 | 21 | @trace :do_three 22 | def do_three(value) do 23 | Process.sleep(2) 24 | value 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /examples/apps/absinthe_example/lib/router.ex: -------------------------------------------------------------------------------- 1 | defmodule AbsintheExample.Router do 2 | use Plug.Builder 3 | use Plug.ErrorHandler 4 | 5 | plug Plug.Parsers, 6 | parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser], 7 | pass: ["*/*"], 8 | json_decoder: Jason 9 | 10 | plug Absinthe.Plug, schema: AbsintheExample.Schema 11 | 12 | def handle_errors(conn, error) do 13 | send_resp(conn, conn.status, "Something went wrong: #{inspect(error)}") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/apps/absinthe_example/lib/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule AbsintheExample.Schema do 2 | use Absinthe.Schema 3 | 4 | query do 5 | field :echo, :string do 6 | arg :this, :string 7 | resolve &AbsintheExample.Resolvers.echo/3 8 | end 9 | 10 | field :one, :one_thing do 11 | resolve &AbsintheExample.Resolvers.one/3 12 | end 13 | end 14 | 15 | object :one_thing do 16 | field :two, :two_thing 17 | end 18 | 19 | object :two_thing do 20 | field :three, :integer do 21 | arg :value, :integer 22 | resolve &AbsintheExample.Resolvers.three/3 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /examples/apps/absinthe_example/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AbsintheExample.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :absinthe_example, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixirc_paths: ["lib", Path.expand(__DIR__ <> "../../../../test/support")], 13 | elixir: "~> 1.9", 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps() 16 | ] 17 | end 18 | 19 | def application do 20 | [ 21 | extra_applications: [:logger], 22 | mod: {AbsintheExample.Application, []} 23 | ] 24 | end 25 | 26 | defp deps do 27 | [ 28 | {:new_relic_agent, path: "../../../"}, 29 | {:absinthe, "~> 1.6"}, 30 | {:absinthe_plug, "~> 1.5"}, 31 | {:plug_cowboy, "~> 2.0"} 32 | ] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /examples/apps/absinthe_example/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | /tmp/ 23 | 24 | # Secrets file for local development 25 | secret.exs 26 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/README.md: -------------------------------------------------------------------------------- 1 | # Ecto 2 | 3 | An example of an `Ecto` database app instrumented by the New Relic Agent via `telemetry`. 4 | 5 | This instrumentation is fully automatic. 6 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ecto_example, 4 | http_port: 4001, 5 | ecto_repos: [EctoExample.PostgresRepo, EctoExample.MySQLRepo, EctoExample.SQLite3Repo] 6 | 7 | config :ecto_example, EctoExample.PostgresRepo, 8 | database: "example_db", 9 | username: "postgres", 10 | password: "password", 11 | hostname: "localhost", 12 | port: 5432 13 | 14 | config :ecto_example, EctoExample.MySQLRepo, 15 | database: "example_db", 16 | username: "root", 17 | password: "password", 18 | hostname: "localhost", 19 | port: 3306 20 | 21 | config :ecto_example, EctoExample.SQLite3Repo, database: "tmp/example_db.sqlite3" 22 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/lib/ecto_example/application.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoExample.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | http_port = Application.get_env(:ecto_example, :http_port) 10 | 11 | children = [ 12 | EctoExample.Database, 13 | Plug.Cowboy.child_spec(scheme: :http, plug: EctoExample.Router, options: [port: http_port]) 14 | ] 15 | 16 | opts = [strategy: :one_for_one, name: EctoExample.Supervisor] 17 | Supervisor.start_link(children, opts) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/lib/ecto_example/count.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoExample.Count do 2 | use Ecto.Schema 3 | 4 | def all do 5 | import Ecto.Query 6 | from(c in EctoExample.Count, select: c) 7 | end 8 | 9 | schema "counts" do 10 | timestamps() 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/lib/ecto_example/database.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoExample.Database do 2 | use GenServer 3 | 4 | def start_link(_) do 5 | GenServer.start_link(__MODULE__, :ok) 6 | end 7 | 8 | def init(:ok) do 9 | start_and_migrate(EctoExample.PostgresRepo) 10 | start_and_migrate(EctoExample.MySQLRepo) 11 | start_and_migrate(EctoExample.SQLite3Repo) 12 | 13 | {:ok, %{}} 14 | end 15 | 16 | def start_and_migrate(repo) do 17 | config = Application.get_env(:ecto_example, repo) 18 | 19 | adapter = repo.__adapter__() 20 | adapter.storage_down(config) 21 | :ok = adapter.storage_up(config) 22 | 23 | repo.start_link() 24 | Ecto.Migrator.up(repo, 0, EctoExample.Migration) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/lib/ecto_example/migration.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoExample.Migration do 2 | use Ecto.Migration 3 | 4 | def up do 5 | create table("counts") do 6 | timestamps() 7 | end 8 | 9 | # used to trigger an Error in router 10 | create(index(:counts, :inserted_at, unique: true)) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/lib/ecto_example/mysql_repo.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoExample.MySQLRepo do 2 | use Ecto.Repo, 3 | otp_app: :ecto_example, 4 | adapter: Ecto.Adapters.MyXQL 5 | end 6 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/lib/ecto_example/postgres_repo.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoExample.PostgresRepo do 2 | use Ecto.Repo, 3 | otp_app: :ecto_example, 4 | adapter: Ecto.Adapters.Postgres 5 | end 6 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/lib/ecto_example/router.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoExample.Router do 2 | use Plug.Router 3 | 4 | plug(:match) 5 | plug(:dispatch) 6 | 7 | get "/hello" do 8 | error_query(EctoExample.PostgresRepo) 9 | error_query(EctoExample.MySQLRepo) 10 | error_query(EctoExample.SQLite3Repo) 11 | 12 | count_query(EctoExample.PostgresRepo) 13 | count_query(EctoExample.MySQLRepo) 14 | count_query(EctoExample.SQLite3Repo) 15 | 16 | stream_query(EctoExample.PostgresRepo) 17 | stream_query(EctoExample.MySQLRepo) 18 | stream_query(EctoExample.SQLite3Repo) 19 | 20 | delete_query(EctoExample.PostgresRepo) 21 | delete_query(EctoExample.MySQLRepo) 22 | delete_query(EctoExample.SQLite3Repo) 23 | 24 | send_resp(conn, 200, Jason.encode!(%{hello: "world"})) 25 | end 26 | 27 | match _ do 28 | send_resp(conn, 404, "oops") 29 | end 30 | 31 | def stream_query(repo) do 32 | repo.transaction(fn -> 33 | EctoExample.Count.all() 34 | |> repo.stream() 35 | |> Enum.to_list() 36 | end) 37 | |> case do 38 | {:ok, [_ | _]} -> :good 39 | end 40 | end 41 | 42 | def delete_query(repo) do 43 | repo.get!(EctoExample.Count, 1) 44 | |> repo.delete! 45 | end 46 | 47 | def count_query(repo) do 48 | {:ok, %{id: id}} = repo.insert(%EctoExample.Count{}) 49 | record = repo.get!(EctoExample.Count, id) |> Ecto.Changeset.change() 50 | repo.update!(record, force: true) 51 | 52 | repo.aggregate(EctoExample.Count, :count) 53 | |> case do 54 | n when n > 1 -> :good 55 | end 56 | end 57 | 58 | def error_query(repo) do 59 | # The migration has a unique index on inserted_at 60 | # This triggers an error that the agent should capture 61 | ts = ~N[2020-01-17 10:00:00] 62 | 63 | {:ok, %{id: _id}} = repo.insert(%EctoExample.Count{inserted_at: ts}) 64 | {:error, _} = repo.insert(%EctoExample.Count{inserted_at: ts}) 65 | rescue 66 | Ecto.ConstraintError -> nil 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/lib/ecto_example/sqlite3_repo.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoExample.SQLite3Repo do 2 | use Ecto.Repo, 3 | otp_app: :ecto_example, 4 | adapter: Ecto.Adapters.SQLite3 5 | end 6 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoExample.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ecto_example, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | deps_path: "../../deps", 10 | config_path: "../../config/config.exs", 11 | lockfile: "../../mix.lock", 12 | elixirc_paths: ["lib", Path.expand(__DIR__ <> "../../../../test/support")], 13 | elixir: "~> 1.9", 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps() 16 | ] 17 | end 18 | 19 | # Run "mix help compile.app" to learn about applications. 20 | def application do 21 | [ 22 | extra_applications: [:logger], 23 | mod: {EctoExample.Application, []} 24 | ] 25 | end 26 | 27 | # Run "mix help deps" to learn about dependencies. 28 | defp deps do 29 | [ 30 | {:new_relic_agent, path: "../../../"}, 31 | {:plug_cowboy, "~> 2.0"}, 32 | {:ecto_sql, "~> 3.9"}, 33 | {:postgrex, ">= 0.0.0"}, 34 | {:myxql, ">= 0.0.0"}, 35 | {:ecto_sqlite3, ">= 0.0.0"} 36 | ] 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /examples/apps/ecto_example/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/apps/instrumented_task/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /examples/apps/instrumented_task/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | instrumented_task-*.tar 24 | 25 | -------------------------------------------------------------------------------- /examples/apps/instrumented_task/README.md: -------------------------------------------------------------------------------- 1 | # InstrumentedTask 2 | 3 | Example of an instrumented `Mix.Task` 4 | -------------------------------------------------------------------------------- /examples/apps/instrumented_task/lib/instrumented_task.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.InstrumentedTask do 2 | use Mix.Task 3 | use NewRelic.Instrumented.Mix.Task 4 | 5 | def run(_) do 6 | IO.puts("Instrumented Task exectued") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /examples/apps/instrumented_task/lib/uninstrumented_task.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.UninstrumentedTask do 2 | use Mix.Task 3 | 4 | @moduledoc """ 5 | If the new_relic_agent application isn't even started, 6 | calls to instrumentation functions should not fail 7 | """ 8 | def run(_) do 9 | NewRelic.report_custom_metric("My/Metric", 123) 10 | 11 | IO.puts("Uninstrumented Task exectued") 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /examples/apps/instrumented_task/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule InstrumentedTask.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :instrumented_task, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixirc_paths: ["lib", Path.expand(__DIR__ <> "../../../../test/support")], 13 | elixir: "~> 1.9", 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps() 16 | ] 17 | end 18 | 19 | def application do 20 | [ 21 | extra_applications: [:logger] 22 | ] 23 | end 24 | 25 | defp deps do 26 | [ 27 | {:new_relic_agent, path: "../../../"} 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /examples/apps/oban_example/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /examples/apps/oban_example/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | oban_example-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /examples/apps/oban_example/README.md: -------------------------------------------------------------------------------- 1 | # ObanExample 2 | 3 | An example app demonstrating auto-instrumentation of Oban 4 | -------------------------------------------------------------------------------- /examples/apps/oban_example/lib/oban_example/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ObanExample.Application do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | config = [ 6 | notifier: Oban.Notifiers.PG, 7 | testing: :inline 8 | ] 9 | 10 | children = [{Oban, config}] 11 | 12 | opts = [strategy: :one_for_one, name: ObanExample.Supervisor] 13 | Supervisor.start_link(children, opts) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/apps/oban_example/lib/oban_example/worker.ex: -------------------------------------------------------------------------------- 1 | defmodule ObanExample.Worker do 2 | use Oban.Worker 3 | 4 | @impl Oban.Worker 5 | def perform(%Oban.Job{args: %{"error" => message}}) do 6 | {:error, message} 7 | end 8 | 9 | def perform(%Oban.Job{args: _args}) do 10 | Process.sleep(15 + :rand.uniform(50)) 11 | :ok 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /examples/apps/oban_example/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ObanExample.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :oban_example, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixirc_paths: ["lib", Path.expand(__DIR__ <> "../../../../test/support")], 13 | elixir: "~> 1.16", 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps() 16 | ] 17 | end 18 | 19 | # Run "mix help compile.app" to learn about applications. 20 | def application do 21 | [ 22 | extra_applications: [:logger], 23 | mod: {ObanExample.Application, []} 24 | ] 25 | end 26 | 27 | # Run "mix help deps" to learn about dependencies. 28 | defp deps do 29 | [ 30 | {:new_relic_agent, path: "../../../"}, 31 | {:oban, "~> 2.0"} 32 | ] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /examples/apps/oban_example/test/oban_example_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ObanExampleTest do 2 | use ExUnit.Case 3 | 4 | alias NewRelic.Harvest.Collector 5 | 6 | setup_all do 7 | TestHelper.simulate_agent_enabled() 8 | TestHelper.simulate_agent_run() 9 | end 10 | 11 | test "instruments a job" do 12 | TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle) 13 | TestHelper.restart_harvest_cycle(Collector.TransactionEvent.HarvestCycle) 14 | 15 | ObanExample.Worker.new(%{some: "args"}, tags: ["foo", "bar"]) 16 | |> Oban.insert() 17 | 18 | metrics = TestHelper.gather_harvest(Collector.Metric.Harvester) 19 | events = TestHelper.gather_harvest(Collector.TransactionEvent.Harvester) 20 | 21 | assert TestHelper.find_metric( 22 | metrics, 23 | "OtherTransaction/Oban/default/ObanExample.Worker/perform", 24 | 1 25 | ) 26 | 27 | event = 28 | TestHelper.find_event(events, "OtherTransaction/Oban/default/ObanExample.Worker/perform") 29 | 30 | assert event[:timestamp] |> is_number 31 | assert event[:duration] >= 0.015 32 | assert event[:duration] <= 0.065 33 | assert event[:duration] <= 0.065 34 | assert event[:"oban.worker"] == "ObanExample.Worker" 35 | assert event[:"oban.queue"] == "default" 36 | assert event[:"oban.job.result"] == "success" 37 | assert event[:"oban.job.tags"] == "foo,bar" 38 | end 39 | 40 | test "instruments a failed job" do 41 | TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle) 42 | TestHelper.restart_harvest_cycle(Collector.TransactionEvent.HarvestCycle) 43 | TestHelper.restart_harvest_cycle(Collector.TransactionErrorEvent.HarvestCycle) 44 | 45 | ObanExample.Worker.new(%{error: "error!"}, tags: ["foo", "bar"]) 46 | |> Oban.insert() 47 | 48 | metrics = TestHelper.gather_harvest(Collector.Metric.Harvester) 49 | events = TestHelper.gather_harvest(Collector.TransactionEvent.Harvester, 0) 50 | error_events = TestHelper.gather_harvest(Collector.TransactionErrorEvent.Harvester, 0) 51 | 52 | assert TestHelper.find_metric( 53 | metrics, 54 | "OtherTransaction/Oban/default/ObanExample.Worker/perform", 55 | 1 56 | ) 57 | 58 | event = 59 | TestHelper.find_event(events, "OtherTransaction/Oban/default/ObanExample.Worker/perform") 60 | 61 | assert event[:timestamp] |> is_number 62 | assert event[:error] == true 63 | assert event[:"oban.worker"] == "ObanExample.Worker" 64 | assert event[:"oban.queue"] == "default" 65 | assert event[:"oban.job.result"] == "failure" 66 | assert event[:"oban.job.tags"] == "foo,bar" 67 | 68 | error = TestHelper.find_event(error_events, "Oban/default/ObanExample.Worker/perform") 69 | 70 | assert error 71 | 72 | assert error[:"error.message"] =~ 73 | "(Oban.PerformError) ObanExample.Worker failed with {:error, \"error!\"}" 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /examples/apps/oban_example/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/apps/phx_example/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /examples/apps/phx_example/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | phx_example-*.tar 24 | 25 | # Since we are building assets from assets/, 26 | # we ignore priv/static. You may want to comment 27 | # this depending on your deployment strategy. 28 | /priv/static/ 29 | -------------------------------------------------------------------------------- /examples/apps/phx_example/README.md: -------------------------------------------------------------------------------- 1 | # PhxExample 2 | 3 | A trimmed down Phoenix app demonstrating auto-instrumentation. 4 | -------------------------------------------------------------------------------- /examples/apps/phx_example/assets/js/app.js: -------------------------------------------------------------------------------- 1 | import "phoenix_html"; 2 | import { Socket } from "phoenix"; 3 | import { LiveSocket } from "phoenix_live_view"; 4 | 5 | let csrfToken = document 6 | .querySelector("meta[name='csrf-token']") 7 | .getAttribute("content"); 8 | let liveSocket = new LiveSocket("/live", Socket, { 9 | params: { _csrf_token: csrfToken }, 10 | }); 11 | 12 | liveSocket.connect(); 13 | window.liveSocket = liveSocket; 14 | -------------------------------------------------------------------------------- /examples/apps/phx_example/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :phx_example, PhxExampleWeb.Endpoint, 4 | url: [host: "localhost"], 5 | render_errors: [formats: [html: PhxExampleWeb.ErrorHTML], layout: false], 6 | http: [port: 4004], 7 | server: true, 8 | adapter: Phoenix.Endpoint.Cowboy2Adapter, 9 | pubsub_server: PhxExample.PubSub, 10 | live_view: [signing_salt: "dB7qn7EQ"], 11 | secret_key_base: "A+gtEDayUNx4ZyfHvUKETwRC4RjxK0FDlrLjuRhaBnr3Ll3ynfu5RlSSGe5E7zbW" 12 | 13 | config :phx_example, PhxExampleWeb.BanditEndpoint, 14 | url: [host: "localhost"], 15 | render_errors: [formats: [html: PhxExampleWeb.ErrorHTML], layout: false], 16 | http: [port: 4005], 17 | server: true, 18 | adapter: Bandit.PhoenixAdapter, 19 | pubsub_server: PhxExample.PubSub, 20 | live_view: [signing_salt: "dB7qn7EQ"], 21 | secret_key_base: "A+gtEDayUNx4ZyfHvUKETwRC4RjxK0FDlrLjuRhaBnr3Ll3ynfu5RlSSGe5E7zbW" 22 | 23 | config :logger, level: :warning 24 | 25 | config :phoenix, :json_library, Jason 26 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example/application.ex: -------------------------------------------------------------------------------- 1 | defmodule PhxExample.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | def start(_type, _args) do 7 | children = [ 8 | {Phoenix.PubSub, name: PhxExample.PubSub}, 9 | PhxExampleWeb.Endpoint, 10 | PhxExampleWeb.BanditEndpoint 11 | ] 12 | 13 | opts = [strategy: :one_for_one, name: PhxExample.Supervisor] 14 | Supervisor.start_link(children, opts) 15 | end 16 | 17 | def config_change(changed, _new, removed) do 18 | PhxExampleWeb.Endpoint.config_change(changed, removed) 19 | PhxExampleWeb.BanditEndpoint.config_change(changed, removed) 20 | :ok 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web.ex: -------------------------------------------------------------------------------- 1 | defmodule PhxExampleWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, components, channels, and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use PhxExampleWeb, :controller 9 | use PhxExampleWeb, :html 10 | 11 | The definitions below will be executed for every controller, 12 | component, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define additional modules and import 17 | those modules here. 18 | """ 19 | 20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 | 22 | def router do 23 | quote do 24 | use Phoenix.Router, helpers: false 25 | 26 | import Plug.Conn 27 | import Phoenix.Controller 28 | import Phoenix.LiveView.Router 29 | end 30 | end 31 | 32 | def channel do 33 | quote do 34 | use Phoenix.Channel 35 | end 36 | end 37 | 38 | def controller do 39 | quote do 40 | use Phoenix.Controller, 41 | formats: [:html, :json], 42 | layouts: [html: PhxExampleWeb.Layouts] 43 | 44 | import Plug.Conn 45 | 46 | unquote(verified_routes()) 47 | end 48 | end 49 | 50 | def live_view do 51 | quote do 52 | use Phoenix.LiveView, 53 | layout: {PhxExampleWeb.Layouts, :app} 54 | 55 | unquote(html_helpers()) 56 | end 57 | end 58 | 59 | def live_component do 60 | quote do 61 | use Phoenix.LiveComponent 62 | 63 | unquote(html_helpers()) 64 | end 65 | end 66 | 67 | def html do 68 | quote do 69 | use Phoenix.Component 70 | 71 | import Phoenix.Controller, 72 | only: [get_csrf_token: 0, view_module: 1, view_template: 1] 73 | 74 | unquote(html_helpers()) 75 | end 76 | end 77 | 78 | defp html_helpers do 79 | quote do 80 | import Phoenix.HTML 81 | 82 | alias Phoenix.LiveView.JS 83 | 84 | unquote(verified_routes()) 85 | end 86 | end 87 | 88 | def verified_routes do 89 | quote do 90 | use Phoenix.VerifiedRoutes, 91 | endpoint: PhxExampleWeb.Endpoint, 92 | router: PhxExampleWeb.Router, 93 | statics: PhxExampleWeb.static_paths() 94 | end 95 | end 96 | 97 | @doc """ 98 | When used, dispatch to the appropriate controller/view/etc. 99 | """ 100 | defmacro __using__(which) when is_atom(which) do 101 | apply(__MODULE__, which, []) 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/bandit_endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule PhxExampleWeb.BanditEndpoint do 2 | use Phoenix.Endpoint, otp_app: :phx_example 3 | 4 | @session_options [ 5 | store: :cookie, 6 | key: "_phx_example_key", 7 | signing_salt: "F6n7gjjvL6I61gUB", 8 | same_site: "Lax" 9 | ] 10 | 11 | socket "/live", 12 | Phoenix.LiveView.Socket, 13 | websocket: [connect_info: [session: @session_options]], 14 | longpoll: [connect_info: [session: @session_options]] 15 | 16 | plug Plug.Static, 17 | at: "/", 18 | from: :phx_example, 19 | gzip: false, 20 | only: PhxExampleWeb.static_paths() 21 | 22 | plug Plug.RequestId 23 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 24 | 25 | plug Plug.Parsers, 26 | parsers: [:urlencoded, :multipart, :json], 27 | pass: ["*/*"], 28 | json_decoder: Phoenix.json_library() 29 | 30 | plug Plug.MethodOverride 31 | plug Plug.Head 32 | plug Plug.Session, @session_options 33 | plug PhxExampleWeb.Router 34 | end 35 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/components/layouts.ex: -------------------------------------------------------------------------------- 1 | defmodule PhxExampleWeb.Layouts do 2 | use PhxExampleWeb, :html 3 | 4 | embed_templates "layouts/*" 5 | end 6 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/components/layouts/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= @inner_content %> 4 |
5 |
6 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/components/layouts/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <.live_title suffix=" · Phoenix Framework"> 8 | <%= assigns[:page_title] || "PhxExample" %> 9 | 10 | 12 | 13 | 14 | <%= @inner_content %> 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/controllers/error_html.ex: -------------------------------------------------------------------------------- 1 | defmodule PhxExampleWeb.ErrorHTML do 2 | use PhxExampleWeb, :html 3 | 4 | embed_templates "error_html/*" 5 | 6 | def render(template, _assigns) do 7 | Phoenix.Controller.status_message_from_template(template) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/controllers/error_html/500.html.heex: -------------------------------------------------------------------------------- 1 | "Oops, Internal Server Error" 2 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule PhxExampleWeb.PageController do 2 | use PhxExampleWeb, :controller 3 | 4 | def index(conn, _params) do 5 | Process.sleep(300) 6 | render(conn, :index) 7 | end 8 | 9 | def error(_, _) do 10 | Process.sleep(100) 11 | raise "BAD" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/controllers/page_html.ex: -------------------------------------------------------------------------------- 1 | defmodule PhxExampleWeb.PageHTML do 2 | use PhxExampleWeb, :html 3 | 4 | embed_templates "page_html/*" 5 | end 6 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/controllers/page_html/index.html.heex: -------------------------------------------------------------------------------- 1 |
2 |

Welcome to Phoenix!

3 |

Peace-of-mind from prototype to production

4 |
5 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule PhxExampleWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :phx_example 3 | 4 | @session_options [ 5 | store: :cookie, 6 | key: "_phx_example_key", 7 | signing_salt: "F6n7gjjvL6I61gUB", 8 | same_site: "Lax" 9 | ] 10 | 11 | socket "/live", 12 | Phoenix.LiveView.Socket, 13 | websocket: [connect_info: [session: @session_options]], 14 | longpoll: [connect_info: [session: @session_options]] 15 | 16 | plug Plug.Static, 17 | at: "/", 18 | from: :phx_example, 19 | gzip: false, 20 | only: PhxExampleWeb.static_paths() 21 | 22 | plug Plug.RequestId 23 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 24 | 25 | plug Plug.Parsers, 26 | parsers: [:urlencoded, :multipart, :json], 27 | pass: ["*/*"], 28 | json_decoder: Phoenix.json_library() 29 | 30 | plug Plug.MethodOverride 31 | plug Plug.Head 32 | plug Plug.Session, @session_options 33 | plug PhxExampleWeb.Router 34 | end 35 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/live/error_live.ex: -------------------------------------------------------------------------------- 1 | defmodule PhxExampleWeb.ErrorLive do 2 | use PhxExampleWeb, :live_view 3 | 4 | @impl true 5 | def render(assigns) do 6 | ~H""" 7 |
8 |

<%= @some_variable %>

9 |
10 | """ 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/live/home_live.ex: -------------------------------------------------------------------------------- 1 | defmodule PhxExampleWeb.HomeLive do 2 | use PhxExampleWeb, :live_view 3 | 4 | def mount(_params, _session, socket) do 5 | socket = 6 | socket 7 | |> assign(:content, "stuff") 8 | 9 | {:ok, socket} 10 | end 11 | 12 | def handle_event("click", _params, socket) do 13 | socket = 14 | socket 15 | |> assign(:content, "clicked") 16 | 17 | Process.send_after(self(), :after, 1_000) 18 | {:noreply, socket} 19 | end 20 | 21 | def handle_info(:after, socket) do 22 | socket = 23 | socket 24 | |> assign(:content, "after") 25 | 26 | {:noreply, socket} 27 | end 28 | 29 | def render(assigns) do 30 | ~H""" 31 |
32 |

Home

33 |

Some content

34 |

<%= @content %>

35 |
Click me
36 |
37 | """ 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /examples/apps/phx_example/lib/phx_example_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule PhxExampleWeb.Router do 2 | use PhxExampleWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, html: {PhxExampleWeb.Layouts, :root} 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | scope "/phx", PhxExampleWeb do 14 | pipe_through :browser 15 | 16 | live "/home", HomeLive, :index 17 | live "/live_error", ErrorLive, :index 18 | 19 | get "/error", PageController, :error 20 | get "/:foo", PageController, :index 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /examples/apps/phx_example/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PhxExample.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :phx_example, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixirc_paths: ["lib", Path.expand(__DIR__ <> "../../../../test/support")], 13 | elixir: "~> 1.7", 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps() 16 | ] 17 | end 18 | 19 | def application do 20 | [ 21 | mod: {PhxExample.Application, []}, 22 | extra_applications: [:logger, :runtime_tools] 23 | ] 24 | end 25 | 26 | defp deps do 27 | [ 28 | {:new_relic_agent, path: "../../../"}, 29 | {:phoenix, "~> 1.5"}, 30 | {:phoenix_html, "~> 3.3"}, 31 | {:phoenix_view, "~> 2.0"}, 32 | {:phoenix_live_view, "~> 0.20"}, 33 | {:floki, ">= 0.30.0", only: :test}, 34 | {:jason, "~> 1.0"}, 35 | {:plug_cowboy, "~> 2.0"}, 36 | {:bandit, "~> 1.0"} 37 | ] 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /examples/apps/phx_example/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/apps/redix_example/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /examples/apps/redix_example/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | redix_example-*.tar 24 | 25 | -------------------------------------------------------------------------------- /examples/apps/redix_example/README.md: -------------------------------------------------------------------------------- 1 | # RedixExample 2 | 3 | An example app demonstrating auto-instrumentation of a Redis DB 4 | -------------------------------------------------------------------------------- /examples/apps/redix_example/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :redix_example, 4 | http_port: 4003 5 | -------------------------------------------------------------------------------- /examples/apps/redix_example/lib/redix_example/application.ex: -------------------------------------------------------------------------------- 1 | defmodule RedixExample.Application do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | http_port = Application.get_env(:redix_example, :http_port) 6 | 7 | children = [ 8 | {Redix, host: "localhost", name: :redix, sync_connect: true}, 9 | Plug.Cowboy.child_spec(scheme: :http, plug: RedixExample.Router, options: [port: http_port]) 10 | ] 11 | 12 | opts = [strategy: :one_for_one, name: RedixExample.Supervisor] 13 | Supervisor.start_link(children, opts) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/apps/redix_example/lib/redix_example/router.ex: -------------------------------------------------------------------------------- 1 | defmodule RedixExample.Router do 2 | use Plug.Router 3 | 4 | plug(:match) 5 | plug(:dispatch) 6 | 7 | get "/hello" do 8 | {:ok, _} = Redix.command(:redix, ["SET", "mykey", "foo"]) 9 | {:ok, "foo"} = Redix.command(:redix, ["GET", "mykey"]) 10 | 11 | {:ok, [_, _, _, "2"]} = 12 | Redix.pipeline(:redix, [ 13 | ["DEL", "counter"], 14 | ["INCR", "counter"], 15 | ["INCR", "counter"], 16 | ["GET", "counter"] 17 | ]) 18 | 19 | {:ok, pid} = Redix.start_link("redis://localhost") 20 | {:ok, _} = Redix.command(pid, ["HSET", "myHash", "myKey", "foo"]) 21 | :ok = Redix.stop(pid) 22 | 23 | send_resp(conn, 200, Jason.encode!(%{hello: "world"})) 24 | end 25 | 26 | get "/err" do 27 | {:error, _} = Redix.pipeline(:redix, [["PING"], ["PING"]], timeout: 0) 28 | 29 | send_resp(conn, 200, Jason.encode!(%{bad: "news"})) 30 | end 31 | 32 | match _ do 33 | send_resp(conn, 404, "oops") 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /examples/apps/redix_example/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule RedixExample.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :redix_example, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixirc_paths: ["lib", Path.expand(__DIR__ <> "../../../../test/support")], 13 | elixir: "~> 1.9", 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps() 16 | ] 17 | end 18 | 19 | # Run "mix help compile.app" to learn about applications. 20 | def application do 21 | [ 22 | extra_applications: [:logger], 23 | mod: {RedixExample.Application, []} 24 | ] 25 | end 26 | 27 | # Run "mix help deps" to learn about dependencies. 28 | defp deps do 29 | [ 30 | {:new_relic_agent, path: "../../../"}, 31 | {:plug_cowboy, "~> 2.0"}, 32 | {:redix, "~> 1.0"} 33 | ] 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /examples/apps/redix_example/test/redix_example_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RedixExampleTest do 2 | use ExUnit.Case 3 | 4 | alias NewRelic.Harvest.Collector 5 | 6 | setup_all do 7 | TestHelper.simulate_agent_enabled() 8 | TestHelper.simulate_agent_run() 9 | end 10 | 11 | test "Redix queries" do 12 | TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle) 13 | TestHelper.restart_harvest_cycle(Collector.SpanEvent.HarvestCycle) 14 | 15 | {:ok, %{body: body}} = request("/hello") 16 | assert body =~ "world" 17 | 18 | metrics = TestHelper.gather_harvest(Collector.Metric.Harvester) 19 | 20 | assert TestHelper.find_metric( 21 | metrics, 22 | "Datastore/Redis/all", 23 | 4 24 | ) 25 | 26 | assert TestHelper.find_metric( 27 | metrics, 28 | "Datastore/Redis/allWeb", 29 | 4 30 | ) 31 | 32 | assert TestHelper.find_metric( 33 | metrics, 34 | "Datastore/allWeb", 35 | 4 36 | ) 37 | 38 | assert TestHelper.find_metric( 39 | metrics, 40 | "Datastore/operation/Redis/SET", 41 | 1 42 | ) 43 | 44 | assert TestHelper.find_metric( 45 | metrics, 46 | "Datastore/operation/Redis/HSET", 47 | 1 48 | ) 49 | 50 | assert TestHelper.find_metric( 51 | metrics, 52 | {"Datastore/operation/Redis/SET", "WebTransaction/Plug/GET/hello"}, 53 | 1 54 | ) 55 | 56 | span_events = TestHelper.gather_harvest(Collector.SpanEvent.Harvester) 57 | 58 | get_event = TestHelper.find_event(span_events, "Datastore/operation/Redis/GET") 59 | 60 | assert get_event[:"peer.address"] == "localhost:6379" 61 | assert get_event[:"db.statement"] == "GET mykey" 62 | assert get_event[:"redix.connection"] =~ "PID" 63 | assert get_event[:"redix.connection_name"] == ":redix" 64 | assert get_event[:timestamp] |> is_number 65 | assert get_event[:duration] > 0.0 66 | 67 | pipeline_event = TestHelper.find_event(span_events, "Datastore/operation/Redis/PIPELINE") 68 | 69 | assert pipeline_event[:"peer.address"] == "localhost:6379" 70 | 71 | assert pipeline_event[:"db.statement"] == 72 | "DEL counter; INCR counter; INCR counter; GET counter" 73 | 74 | hset_event = TestHelper.find_event(span_events, "Datastore/operation/Redis/HSET") 75 | 76 | assert hset_event[:"peer.address"] == "localhost:6379" 77 | end 78 | 79 | test "Redix error" do 80 | TestHelper.restart_harvest_cycle(Collector.SpanEvent.HarvestCycle) 81 | 82 | {:ok, %{body: body}} = request("/err") 83 | assert body =~ "bad" 84 | 85 | span_events = TestHelper.gather_harvest(Collector.SpanEvent.Harvester) 86 | 87 | err_event = TestHelper.find_event(span_events, "Datastore/operation/Redis/PIPELINE") 88 | 89 | assert err_event[:"peer.address"] == "localhost:6379" 90 | # On elixir 1.14 OTP 26, the error message is "unknown POSIX error: timeout" 91 | # On elixir 1.12, the error message is " :timeout" 92 | assert err_event[:"redix.error"] =~ "timeout" 93 | end 94 | 95 | defp request(path) do 96 | http_port = Application.get_env(:redix_example, :http_port) 97 | NewRelic.Util.HTTP.get("http://localhost:#{http_port}#{path}") 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /examples/apps/redix_example/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/apps/w3c_trace_context_validation/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /examples/apps/w3c_trace_context_validation/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | w3c_trace_context_validation-*.tar 24 | 25 | /trace-context/ 26 | 27 | -------------------------------------------------------------------------------- /examples/apps/w3c_trace_context_validation/README.md: -------------------------------------------------------------------------------- 1 | # W3cTraceContextValidation 2 | 3 | This is an app that implements the W3C Trace Context Validation Service: 4 | 5 | * https://github.com/w3c/trace-context/tree/master/test 6 | 7 | ----- 8 | 9 | Start the app: 10 | 11 | ``` 12 | env NEW_RELIC_HARVEST_ENABLED=true iex -S mix 13 | ``` 14 | 15 | Run the test: 16 | 17 | ``` 18 | git clone https://github.com/w3c/trace-context.git 19 | cd trace-context 20 | pip3 install aiohttp 21 | python3 test/test.py http://127.0.0.1:4002/test 22 | ``` 23 | -------------------------------------------------------------------------------- /examples/apps/w3c_trace_context_validation/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :w3c_trace_context_validation, 4 | http_port: 4002 5 | -------------------------------------------------------------------------------- /examples/apps/w3c_trace_context_validation/lib/application.ex: -------------------------------------------------------------------------------- 1 | defmodule W3cTraceContextValidation.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | http_port = Application.get_env(:w3c_trace_context_validation, :http_port) 10 | 11 | children = [ 12 | Plug.Cowboy.child_spec( 13 | scheme: :http, 14 | plug: W3cTraceContextValidation.Router, 15 | options: [port: http_port] 16 | ) 17 | ] 18 | 19 | opts = [strategy: :one_for_one, name: W3cTraceContextValidation.Supervisor] 20 | Supervisor.start_link(children, opts) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /examples/apps/w3c_trace_context_validation/lib/router.ex: -------------------------------------------------------------------------------- 1 | defmodule W3cTraceContextValidation.Router do 2 | use Plug.Router 3 | use NewRelic.Tracer 4 | 5 | plug(Plug.Parsers, parsers: [:json], json_decoder: Jason) 6 | 7 | plug(:match) 8 | plug(:dispatch) 9 | 10 | post "/test" do 11 | directions = conn.body_params["_json"] 12 | 13 | for %{"url" => url, "arguments" => arguments} <- directions do 14 | request(url, arguments) 15 | end 16 | 17 | send_resp(conn, 200, "ok") 18 | end 19 | 20 | match _ do 21 | send_resp(conn, 404, "oops") 22 | end 23 | 24 | @trace {:request, category: :external} 25 | def request(url, arguments) do 26 | NewRelic.set_span(:http, url: url, method: :post, component: "HTTPoison") 27 | 28 | HTTPoison.post( 29 | url, 30 | Jason.encode!(arguments), 31 | NewRelic.distributed_trace_headers(:http) 32 | ) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /examples/apps/w3c_trace_context_validation/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule W3cTraceContextValidation.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :w3c_trace_context_validation, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixirc_paths: ["lib", Path.expand(__DIR__ <> "../../../../test/support")], 13 | elixir: "~> 1.9", 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps() 16 | ] 17 | end 18 | 19 | def application do 20 | [ 21 | extra_applications: [:logger], 22 | mod: {W3cTraceContextValidation.Application, []} 23 | ] 24 | end 25 | 26 | defp deps do 27 | [ 28 | {:new_relic_agent, path: "../../../"}, 29 | {:plug_cowboy, "~> 2.0"}, 30 | {:httpoison, "~> 1.0"} 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /examples/apps/w3c_trace_context_validation/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /examples/apps/w3c_trace_context_validation/test/w3c_trace_context_validation_test.exs: -------------------------------------------------------------------------------- 1 | defmodule W3cTraceContextValidationTest do 2 | use ExUnit.Case 3 | 4 | test "greets the world" do 5 | assert {:ok, %{status_code: 404}} = HTTPoison.get("http://localhost:#{port()}/hello") 6 | end 7 | 8 | test "validator" do 9 | assert {:ok, %{status_code: 200}} = 10 | HTTPoison.post("http://localhost:#{port()}/test", "[]", 11 | "content-type": "application/json" 12 | ) 13 | end 14 | 15 | def port() do 16 | Application.get_env(:w3c_trace_context_validation, :http_port) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /examples/config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :logger, level: :debug 4 | 5 | config :new_relic_agent, 6 | app_name: "ExampleApps", 7 | trusted_account_key: "trusted_account_key", 8 | license_key: "license_key", 9 | bypass_collector: true 10 | 11 | for config <- "../apps/*/config/config.exs" |> Path.expand(__DIR__) |> Path.wildcard() do 12 | import_config config 13 | end 14 | 15 | if Mix.env() != :test do 16 | if File.exists?(Path.expand("./secret.exs", __DIR__)), 17 | do: import_config("secret.exs") 18 | end 19 | -------------------------------------------------------------------------------- /examples/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres_db: 3 | image: postgres 4 | environment: 5 | POSTGRES_PASSWORD: password 6 | ports: 7 | - 5432:5432 8 | mysql_db: 9 | image: mysql 10 | environment: 11 | MYSQL_ROOT_PASSWORD: password 12 | ports: 13 | - 3306:3306 14 | redis_db: 15 | image: redis 16 | ports: 17 | - 6379:6379 18 | -------------------------------------------------------------------------------- /examples/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Examples.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | apps_path: "apps", 7 | version: "0.1.0", 8 | start_permanent: Mix.env() == :prod, 9 | deps: deps() 10 | ] 11 | end 12 | 13 | defp deps do 14 | [ 15 | {:ecto, ">= 3.9.5"} 16 | ] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/new_relic/aggregate/aggregate.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Aggregate do 2 | defstruct meta: %{}, values: %{} 3 | 4 | # A metric-like struct for reporting aggregate data as events to NRDB 5 | 6 | @moduledoc false 7 | 8 | def merge(aggregate, values) do 9 | new_values = Map.merge(aggregate.values, values, fn _k, v1, v2 -> v1 + v2 end) 10 | %{aggregate | values: new_values} 11 | end 12 | 13 | def annotate(aggregate) do 14 | aggregate.values 15 | |> Map.merge(averages(aggregate.values)) 16 | |> Map.merge(aggregate.meta) 17 | |> Map.put(:category, :Metric) 18 | end 19 | 20 | defp averages(%{call_count: call_count} = values) do 21 | values 22 | |> Enum.reject(fn {key, _value} -> key == :call_count end) 23 | |> Map.new(fn {key, value} -> {:"avg_#{key}", value / call_count} end) 24 | end 25 | 26 | defp averages(_values), do: %{} 27 | end 28 | -------------------------------------------------------------------------------- /lib/new_relic/aggregate/reporter.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Aggregate.Reporter do 2 | use GenServer 3 | alias NewRelic.Aggregate 4 | 5 | # This GenServer collects aggregate metric measurements, aggregates them, 6 | # and reports them to the Harvester at the defined sample_cycle 7 | 8 | @moduledoc false 9 | 10 | def start_link(_) do 11 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 12 | end 13 | 14 | def init(:ok) do 15 | NewRelic.sample_process() 16 | if NewRelic.Config.enabled?(), do: send(self(), :report) 17 | {:ok, %{}} 18 | end 19 | 20 | def report_aggregate(meta, values), do: GenServer.cast(__MODULE__, {:aggregate, meta, values}) 21 | 22 | def handle_cast({:aggregate, meta, values}, state) do 23 | metric = 24 | state 25 | |> Map.get(meta, %Aggregate{meta: meta}) 26 | |> Aggregate.merge(values) 27 | 28 | {:noreply, Map.put(state, meta, metric)} 29 | end 30 | 31 | def handle_info(:report, state) do 32 | record_aggregates(state) 33 | Process.send_after(self(), :report, NewRelic.Sampler.Reporter.sample_cycle()) 34 | {:noreply, %{}} 35 | end 36 | 37 | def handle_call(:report, _from, state) do 38 | record_aggregates(state) 39 | {:reply, :ok, %{}} 40 | end 41 | 42 | defp record_aggregates(state) do 43 | Enum.each(state, fn {_meta, metric} -> 44 | NewRelic.report_custom_event(aggregate_event_type(), Aggregate.annotate(metric)) 45 | end) 46 | end 47 | 48 | defp aggregate_event_type, 49 | do: Application.get_env(:new_relic_agent, :aggregate_event_type, "ElixirAggregate") 50 | end 51 | -------------------------------------------------------------------------------- /lib/new_relic/aggregate/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Aggregate.Supervisor do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | def start_link(_) do 7 | Supervisor.start_link(__MODULE__, []) 8 | end 9 | 10 | def init(_) do 11 | children = [ 12 | NewRelic.Aggregate.Reporter 13 | ] 14 | 15 | Supervisor.init(children, strategy: :one_for_one) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/new_relic/always_on_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.AlwaysOnSupervisor do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | def start_link(_) do 7 | Supervisor.start_link(__MODULE__, []) 8 | end 9 | 10 | def init(_) do 11 | children = [ 12 | NewRelic.Harvest.Collector.AgentRun, 13 | NewRelic.Harvest.HarvesterStore, 14 | NewRelic.DistributedTrace.Supervisor, 15 | NewRelic.Transaction.Supervisor 16 | ] 17 | 18 | Supervisor.init(children, strategy: :one_for_one) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/new_relic/application.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Application do 2 | use Application 3 | 4 | @moduledoc false 5 | 6 | def start(_type, _args) do 7 | NewRelic.Init.run() 8 | NewRelic.SignalHandler.start() 9 | 10 | children = [ 11 | NewRelic.Logger, 12 | NewRelic.AlwaysOnSupervisor, 13 | NewRelic.EnabledSupervisorManager, 14 | NewRelic.Telemetry.Supervisor, 15 | NewRelic.GracefulShutdown 16 | ] 17 | 18 | Supervisor.start_link(children, strategy: :one_for_one, name: NewRelic.Supervisor) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/new_relic/custom/event.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Custom.Event do 2 | defstruct type: nil, 3 | timestamp: nil, 4 | attributes: %{} 5 | 6 | @moduledoc false 7 | 8 | # Struct for reporting Custom events 9 | 10 | def format_events(events) do 11 | Enum.map(events, &format_event/1) 12 | end 13 | 14 | defp format_event(%__MODULE__{} = event) do 15 | [ 16 | %{ 17 | type: event.type, 18 | timestamp: event.timestamp 19 | }, 20 | NewRelic.Util.Event.process_event(event.attributes), 21 | %{} 22 | ] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/new_relic/distributed_trace/backoff_sampler.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.DistributedTrace.BackoffSampler do 2 | use GenServer 3 | alias NewRelic.Harvest.Collector.AgentRun 4 | 5 | # This GenServer tracks the sampling rate across sampling periods, 6 | # which is used to determine when to sample a Distributed Trace. 7 | # State is stored in erlang `counters` which are super fast 8 | 9 | @moduledoc false 10 | 11 | def start_link(_) do 12 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 13 | end 14 | 15 | # Counter indexes 16 | @size 5 17 | @cycle_number 1 18 | @sampled_true_count 2 19 | @decided_count 3 20 | @decided_count_last 4 21 | @sampling_target 5 22 | 23 | def init(:ok) do 24 | NewRelic.sample_process() 25 | 26 | :persistent_term.put({__MODULE__, :counter}, new(@size, [])) 27 | put(@sampling_target, AgentRun.lookup(:sampling_target) || 10) 28 | 29 | trigger_next_cycle() 30 | {:ok, %{}} 31 | end 32 | 33 | def sample? do 34 | calculate(%{ 35 | cycle_number: get(@cycle_number), 36 | sampled_true_count: get(@sampled_true_count), 37 | decided_count: get(@decided_count), 38 | decided_count_last: get(@decided_count_last), 39 | sampling_target: get(@sampling_target) 40 | }) 41 | end 42 | 43 | def handle_info(:cycle, state) do 44 | cycle() 45 | trigger_next_cycle() 46 | {:noreply, state} 47 | end 48 | 49 | def reset() do 50 | put(@cycle_number, 0) 51 | put(@decided_count_last, 0) 52 | put(@decided_count, 0) 53 | put(@sampled_true_count, 0) 54 | end 55 | 56 | def cycle() do 57 | incr(@cycle_number) 58 | put(@decided_count_last, get(@decided_count)) 59 | put(@decided_count, 0) 60 | put(@sampled_true_count, 0) 61 | end 62 | 63 | defp calculate(state) do 64 | sampled = do_sample?(state) 65 | update_state(sampled) 66 | sampled 67 | end 68 | 69 | def do_sample?(%{ 70 | cycle_number: 0, 71 | sampled_true_count: sampled_true_count, 72 | sampling_target: sampling_target 73 | }) do 74 | sampled_true_count < sampling_target 75 | end 76 | 77 | def do_sample?(%{ 78 | sampled_true_count: sampled_true_count, 79 | sampling_target: sampling_target, 80 | decided_count_last: decided_count_last 81 | }) 82 | when sampled_true_count < sampling_target do 83 | random(decided_count_last) < sampling_target 84 | end 85 | 86 | def do_sample?(%{ 87 | sampled_true_count: sampled_true_count, 88 | sampling_target: sampling_target, 89 | decided_count: decided_count 90 | }) do 91 | random(decided_count) < 92 | :math.pow(sampling_target, sampling_target / sampled_true_count) - 93 | :math.pow(sampling_target, 0.5) 94 | end 95 | 96 | def trigger_next_cycle() do 97 | cycle_period = AgentRun.lookup(:sampling_target_period) || 60_000 98 | Process.send_after(__MODULE__, :cycle, cycle_period) 99 | end 100 | 101 | defp update_state(false = _sampled?) do 102 | incr(@decided_count) 103 | end 104 | 105 | defp update_state(true = _sampled?) do 106 | incr(@decided_count) 107 | incr(@sampled_true_count) 108 | end 109 | 110 | defp random(0), do: 0 111 | defp random(n), do: :rand.uniform(n) 112 | 113 | @compile {:inline, new: 2, incr: 1, put: 2, get: 1, pt: 0} 114 | defp new(size, opts), do: :counters.new(size, opts) 115 | defp incr(index), do: :counters.add(pt(), index, 1) 116 | defp put(index, value), do: :counters.put(pt(), index, value) 117 | defp get(index), do: :counters.get(pt(), index) 118 | defp pt(), do: :persistent_term.get({__MODULE__, :counter}) 119 | end 120 | -------------------------------------------------------------------------------- /lib/new_relic/distributed_trace/context.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.DistributedTrace.Context do 2 | @moduledoc false 3 | 4 | defstruct type: "App", 5 | source: nil, 6 | version: nil, 7 | account_id: nil, 8 | app_id: nil, 9 | parent_id: nil, 10 | guid: nil, 11 | span_guid: nil, 12 | trace_id: nil, 13 | trust_key: nil, 14 | priority: nil, 15 | sampled: nil, 16 | timestamp: nil 17 | end 18 | -------------------------------------------------------------------------------- /lib/new_relic/distributed_trace/new_relic_context.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.DistributedTrace.NewRelicContext do 2 | @moduledoc false 3 | 4 | alias NewRelic.DistributedTrace.Context 5 | alias NewRelic.Harvest.Collector.AgentRun 6 | 7 | def extract(trace_payload) do 8 | decode(trace_payload) 9 | |> restrict_access 10 | end 11 | 12 | def generate(context) do 13 | encode(context) 14 | end 15 | 16 | def decode(raw_payload) when is_binary(raw_payload) do 17 | with {:ok, json} <- Base.decode64(raw_payload), 18 | {:ok, map} <- NewRelic.JSON.decode(json), 19 | %Context{} = context <- validate(map) do 20 | NewRelic.report_metric(:supportability, [:dt, :accept, :success]) 21 | context 22 | else 23 | error -> 24 | NewRelic.report_metric(:supportability, [:dt, :accept, :parse_error]) 25 | NewRelic.log(:debug, "Bad DT Payload: #{inspect(error)} #{inspect(raw_payload)}") 26 | :bad_dt_payload 27 | end 28 | end 29 | 30 | def restrict_access(:bad_dt_payload), do: :bad_dt_payload 31 | 32 | def restrict_access(%Context{} = context) do 33 | if (context.trust_key || context.account_id) == AgentRun.trusted_account_key() do 34 | context 35 | else 36 | :restricted 37 | end 38 | end 39 | 40 | @payload_version [0, 1] 41 | def validate(%{ 42 | "v" => @payload_version, 43 | "d" => 44 | %{ 45 | "ty" => type, 46 | "ac" => account_id, 47 | "ap" => app_id, 48 | "tr" => trace_id, 49 | "ti" => timestamp 50 | } = data 51 | }) do 52 | %Context{ 53 | source: :new_relic, 54 | version: @payload_version, 55 | type: type, 56 | account_id: account_id, 57 | app_id: app_id, 58 | parent_id: data["tx"], 59 | span_guid: data["id"], 60 | trace_id: trace_id, 61 | trust_key: data["tk"], 62 | priority: data["pr"], 63 | sampled: data["sa"], 64 | timestamp: timestamp 65 | } 66 | end 67 | 68 | def validate(_invalid), do: :invalid 69 | 70 | def encode(context) do 71 | %{ 72 | "v" => @payload_version, 73 | "d" => 74 | %{ 75 | "ty" => context.type, 76 | "ac" => context.account_id |> to_string, 77 | "ap" => context.app_id |> to_string, 78 | "tx" => context.guid, 79 | "tr" => context.trace_id, 80 | "id" => context.span_guid, 81 | "pr" => context.priority, 82 | "sa" => context.sampled, 83 | "ti" => context.timestamp 84 | } 85 | |> maybe_put(:trust_key, "tk", context.account_id, context.trust_key) 86 | } 87 | |> NewRelic.JSON.encode!() 88 | |> Base.encode64() 89 | end 90 | 91 | defp maybe_put(data, :trust_key, _key, account_id, account_id), do: data 92 | defp maybe_put(data, :trust_key, _key, _account_id, nil), do: data 93 | defp maybe_put(data, :trust_key, key, _account_id, trust_key), do: Map.put(data, key, trust_key) 94 | end 95 | -------------------------------------------------------------------------------- /lib/new_relic/distributed_trace/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.DistributedTrace.Supervisor do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | def start_link(_) do 7 | Supervisor.start_link(__MODULE__, []) 8 | end 9 | 10 | def init(_) do 11 | children = [ 12 | NewRelic.DistributedTrace.BackoffSampler 13 | ] 14 | 15 | Supervisor.init(children, strategy: :one_for_one) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/new_relic/distributed_trace/w3c_trace_context/trace_parent.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.DistributedTrace.W3CTraceContext.TraceParent do 2 | @moduledoc false 3 | 4 | # https://w3c.github.io/trace-context/#traceparent-header 5 | 6 | defstruct version: "00", 7 | trace_id: nil, 8 | parent_id: nil, 9 | flags: nil 10 | 11 | @version 2 12 | @trace_id 32 13 | @parent_id 16 14 | @flags 2 15 | 16 | def decode(<<"ff", "-", _::binary>>), 17 | do: invalid() 18 | 19 | def decode(<<_::binary-size(@version), "-", "00000000000000000000000000000000", _::binary>>), 20 | do: invalid() 21 | 22 | def decode(<<_::binary-size(@version), "-", _::binary-size(@trace_id), "-", "0000000000000000", _::binary>>), 23 | do: invalid() 24 | 25 | def decode( 26 | <> 28 | ) do 29 | validate( 30 | [version, trace_id, parent_id, flags], 31 | %__MODULE__{ 32 | version: version, 33 | trace_id: trace_id, 34 | parent_id: parent_id, 35 | flags: %{sampled: flags == "01"} 36 | } 37 | ) 38 | end 39 | 40 | # Future versions can be longer 41 | def decode( 42 | <> 44 | ) 45 | when version != "00" do 46 | validate( 47 | [version, trace_id, parent_id, flags], 48 | %__MODULE__{ 49 | version: version, 50 | trace_id: trace_id, 51 | parent_id: parent_id, 52 | flags: %{sampled: flags == "01"} 53 | } 54 | ) 55 | end 56 | 57 | def decode(_), 58 | do: invalid() 59 | 60 | def encode(%__MODULE__{ 61 | version: _version, 62 | trace_id: trace_id, 63 | parent_id: parent_id, 64 | flags: %{ 65 | sampled: sampled 66 | } 67 | }) do 68 | [ 69 | "00", 70 | String.pad_leading(trace_id, @trace_id, "0") |> String.downcase(), 71 | String.pad_leading(parent_id, @parent_id, "0") |> String.downcase(), 72 | (sampled && "01") || "00" 73 | ] 74 | |> Enum.join("-") 75 | end 76 | 77 | defp invalid() do 78 | NewRelic.report_metric(:supportability, [:trace_context, :traceparent, :invalid]) 79 | :invalid 80 | end 81 | 82 | defp validate(values, context) do 83 | case Enum.all?(values, &valid?/1) do 84 | true -> context 85 | false -> :invalid 86 | end 87 | end 88 | 89 | defp valid?(value), 90 | do: Base.decode16(value, case: :mixed) != :error 91 | end 92 | -------------------------------------------------------------------------------- /lib/new_relic/enabled_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.EnabledSupervisor do 2 | use Supervisor 3 | 4 | # This Supervisor starts processes that we 5 | # only start if the agent is enabled 6 | 7 | @moduledoc false 8 | 9 | def start_link(_) do 10 | Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) 11 | end 12 | 13 | def init(:ok) do 14 | children = [ 15 | NewRelic.Harvest.Supervisor, 16 | NewRelic.LogsInContext.Supervisor, 17 | NewRelic.Sampler.Supervisor, 18 | NewRelic.Error.Supervisor, 19 | NewRelic.Aggregate.Supervisor 20 | ] 21 | 22 | NewRelic.OsMon.start() 23 | 24 | Supervisor.init(children, strategy: :one_for_one) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/new_relic/enabled_supervisor_manager.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.EnabledSupervisorManager do 2 | use DynamicSupervisor 3 | 4 | @moduledoc false 5 | 6 | def start_link(_) do 7 | DynamicSupervisor.start_link(__MODULE__, :ok, name: __MODULE__) 8 | end 9 | 10 | def init(:ok) do 11 | DynamicSupervisor.init(strategy: :one_for_one) 12 | end 13 | 14 | def start_child() do 15 | DynamicSupervisor.start_child(__MODULE__, NewRelic.EnabledSupervisor) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/new_relic/error/event.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Error.Event do 2 | defstruct type: "TransactionError", 3 | timestamp: nil, 4 | error_class: nil, 5 | error_message: nil, 6 | expected: false, 7 | transaction_name: nil, 8 | duration: nil, 9 | database_duration: nil, 10 | user_attributes: %{}, 11 | agent_attributes: %{} 12 | 13 | @moduledoc false 14 | 15 | def format_events(errors) do 16 | Enum.map(errors, &format_event/1) 17 | end 18 | 19 | defp format_event(%__MODULE__{} = error) do 20 | [ 21 | _intrinsic_attributes = %{ 22 | type: error.type, 23 | timestamp: error.timestamp, 24 | "error.class": error.error_class, 25 | "error.message": error.error_message, 26 | "error.expected": error.expected, 27 | transactionName: error.transaction_name, 28 | duration: error.duration, 29 | databaseDuration: error.database_duration 30 | }, 31 | NewRelic.Util.Event.process_event(error.user_attributes), 32 | format_agent_attributes(error.agent_attributes) 33 | ] 34 | end 35 | 36 | defp format_agent_attributes(%{ 37 | http_response_code: http_response_code, 38 | request_method: request_method 39 | }) do 40 | %{ 41 | httpResponseCode: http_response_code, 42 | "request.headers.method": request_method 43 | } 44 | end 45 | 46 | defp format_agent_attributes(_agent_attributes) do 47 | %{} 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/new_relic/error/logger_filter.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Error.LoggerFilter do 2 | @moduledoc false 3 | 4 | # Track errors by attaching a `:logger` primary filter 5 | # Always returns `:ignore` so we don't actually filter anything 6 | 7 | def add_filter() do 8 | :logger.add_primary_filter(__MODULE__, {&__MODULE__.filter/2, []}) 9 | end 10 | 11 | def remove_filter() do 12 | :logger.remove_primary_filter(__MODULE__) 13 | end 14 | 15 | def filter( 16 | %{ 17 | meta: %{error_logger: %{type: :crash_report}}, 18 | msg: {:report, %{report: [report | _]}} 19 | }, 20 | _opts 21 | ) do 22 | if NewRelic.Transaction.Sidecar.tracking?() do 23 | NewRelic.Error.Reporter.CrashReport.report_error(:transaction, report) 24 | else 25 | NewRelic.Error.Reporter.CrashReport.report_error(:process, report) 26 | end 27 | 28 | :ignore 29 | end 30 | 31 | if NewRelic.Util.ConditionalCompile.match?("< 1.15.0") do 32 | def filter( 33 | %{ 34 | meta: %{error_logger: %{tag: :error_msg}}, 35 | msg: {:report, %{label: {_, :terminating}}} 36 | }, 37 | _opts 38 | ) do 39 | :ignore 40 | end 41 | end 42 | 43 | def filter( 44 | %{ 45 | meta: %{error_logger: %{tag: :error_msg}}, 46 | msg: {:report, %{report: %{reason: _} = report}} 47 | }, 48 | _opts 49 | ) do 50 | if NewRelic.Transaction.Sidecar.tracking?() do 51 | NewRelic.Error.Reporter.ErrorMsg.report_error(:transaction, report) 52 | else 53 | NewRelic.Error.Reporter.ErrorMsg.report_error(:process, report) 54 | end 55 | 56 | :ignore 57 | end 58 | 59 | def filter(_log, _opts) do 60 | :ignore 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/new_relic/error/reporter/error_msg.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Error.Reporter.ErrorMsg do 2 | @moduledoc false 3 | 4 | alias NewRelic.Util 5 | alias NewRelic.Harvest.Collector 6 | 7 | def report_error(:transaction, report) do 8 | {exception, stacktrace} = report.reason 9 | process_name = parse_process_name(report[:registered_name], stacktrace) 10 | 11 | NewRelic.add_attributes("error.process": process_name) 12 | 13 | NewRelic.Transaction.Reporter.error(%{ 14 | kind: :error, 15 | reason: exception, 16 | stack: stacktrace 17 | }) 18 | end 19 | 20 | def report_error(:process, report) do 21 | {exception_type, reason, stacktrace, expected} = parse_reason(report.reason) 22 | 23 | process_name = parse_process_name(report[:registered_name], stacktrace) 24 | automatic_attributes = NewRelic.Config.automatic_attributes() 25 | formatted_stacktrace = Util.Error.format_stacktrace(stacktrace, nil) 26 | 27 | Collector.ErrorTrace.Harvester.report_error(%NewRelic.Error.Trace{ 28 | timestamp: System.system_time(:millisecond) / 1_000, 29 | error_type: exception_type, 30 | message: reason, 31 | expected: expected, 32 | stack_trace: formatted_stacktrace, 33 | transaction_name: "OtherTransaction/Elixir/ElixirProcess//#{process_name}", 34 | user_attributes: 35 | Map.merge(automatic_attributes, %{ 36 | process: process_name 37 | }) 38 | }) 39 | 40 | Collector.TransactionErrorEvent.Harvester.report_error(%NewRelic.Error.Event{ 41 | timestamp: System.system_time(:millisecond) / 1_000, 42 | error_class: exception_type, 43 | error_message: reason, 44 | expected: expected, 45 | transaction_name: "OtherTransaction/Elixir/ElixirProcess//#{process_name}", 46 | user_attributes: 47 | Map.merge(automatic_attributes, %{ 48 | process: process_name, 49 | stacktrace: Enum.join(formatted_stacktrace, "\n") 50 | }) 51 | }) 52 | 53 | unless expected do 54 | NewRelic.report_metric({:supportability, :error_event}, error_count: 1) 55 | NewRelic.report_metric(:error, error_count: 1) 56 | end 57 | end 58 | 59 | defp parse_reason({%type{message: message} = exception, stacktrace}) do 60 | expected = parse_error_expected(exception) 61 | type = inspect(type) 62 | reason = "(#{type}) #{message}" 63 | 64 | {type, reason, stacktrace, expected} 65 | end 66 | 67 | defp parse_reason({exception, stacktrace}) do 68 | exception = Exception.normalize(:error, exception, stacktrace) 69 | type = inspect(exception.__struct__) 70 | message = Exception.message(exception) 71 | reason = "(#{type}) #{message}" 72 | 73 | {type, reason, stacktrace, false} 74 | end 75 | 76 | defp parse_process_name([], [{module, _f, _a, _} | _]), do: inspect(module) 77 | defp parse_process_name([], _stacktrace), do: "UnknownProcess" 78 | defp parse_process_name(nil, _stacktrace), do: "UnknownProcess" 79 | defp parse_process_name(registered_name, _stacktrace), do: inspect(registered_name) 80 | 81 | defp parse_error_expected(%{expected: true}), do: true 82 | defp parse_error_expected(_), do: false 83 | end 84 | -------------------------------------------------------------------------------- /lib/new_relic/error/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Error.Supervisor do 2 | use Supervisor 3 | alias NewRelic.Error 4 | 5 | # Registers an erlang error logger to catch and report errors. 6 | 7 | @moduledoc false 8 | 9 | def start_link(_) do 10 | Supervisor.start_link(__MODULE__, []) 11 | end 12 | 13 | def init(_) do 14 | children = [ 15 | {Task.Supervisor, name: Error.TaskSupervisor} 16 | ] 17 | 18 | if NewRelic.Config.feature?(:error_collector) do 19 | add_filter() 20 | end 21 | 22 | Supervisor.init(children, strategy: :one_for_one) 23 | end 24 | 25 | def add_filter(), 26 | do: NewRelic.Error.LoggerFilter.add_filter() 27 | 28 | def remove_filter(), 29 | do: NewRelic.Error.LoggerFilter.remove_filter() 30 | end 31 | -------------------------------------------------------------------------------- /lib/new_relic/error/trace.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Error.Trace do 2 | defstruct timestamp: nil, 3 | transaction_name: "", 4 | message: nil, 5 | expected: false, 6 | error_type: nil, 7 | cat_guid: "", 8 | stack_trace: nil, 9 | agent_attributes: %{}, 10 | user_attributes: %{} 11 | 12 | @moduledoc false 13 | 14 | def format_errors(errors) do 15 | Enum.map(errors, &format_error/1) 16 | end 17 | 18 | defp format_error(%__MODULE__{} = error) do 19 | [ 20 | error.timestamp, 21 | error.transaction_name, 22 | error.message, 23 | error.error_type, 24 | %{ 25 | stack_trace: error.stack_trace, 26 | agentAttributes: format_agent_attributes(error.agent_attributes), 27 | userAttributes: format_user_attributes(error.user_attributes), 28 | intrinsics: format_intrinsic_attributes(error.user_attributes, error) 29 | }, 30 | error.cat_guid 31 | ] 32 | end 33 | 34 | defp format_agent_attributes(%{request_uri: request_uri}) do 35 | %{request_uri: request_uri} 36 | end 37 | 38 | defp format_agent_attributes(_agent_attributes) do 39 | %{} 40 | end 41 | 42 | @intrinsics [:traceId, :guid] 43 | defp format_intrinsic_attributes(user_attributes, error) do 44 | user_attributes 45 | |> Map.take(@intrinsics) 46 | |> Map.merge(%{"error.expected": error.expected}) 47 | end 48 | 49 | defp format_user_attributes(user_attributes) do 50 | user_attributes 51 | |> Map.drop(@intrinsics) 52 | |> Map.new(fn {k, v} -> 53 | (String.Chars.impl_for(v) && {k, v}) || {k, inspect(v)} 54 | end) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/new_relic/error_logger.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.ErrorLogger do 2 | @moduledoc false 3 | require Logger 4 | @behaviour :gen_event 5 | 6 | def init(_) do 7 | Logger.warning("`NewRelic.ErrorLogger` no longer needed, please remove it from :logger configuration") 8 | {:ok, nil} 9 | end 10 | 11 | def handle_call(_opts, state), do: {:ok, :ok, state} 12 | def handle_event(_opts, state), do: {:ok, state} 13 | def handle_info(_opts, state), do: {:ok, state} 14 | def code_change(_old_vsn, state, _extra), do: {:ok, state} 15 | def terminate(_reason, _state), do: :ok 16 | end 17 | -------------------------------------------------------------------------------- /lib/new_relic/graceful_shutdown.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.GracefulShutdown do 2 | @moduledoc false 3 | use GenServer, shutdown: 30_000 4 | 5 | def start_link(_) do 6 | GenServer.start_link(__MODULE__, :ok) 7 | end 8 | 9 | def init(:ok) do 10 | Process.flag(:trap_exit, true) 11 | {:ok, nil} 12 | end 13 | 14 | def terminate(_reason, _state) do 15 | NewRelic.log(:info, "Attempting graceful shutdown") 16 | NewRelic.Error.Supervisor.remove_filter() 17 | NewRelic.Harvest.Supervisor.manual_shutdown() 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/collector/connect.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.Collector.Connect do 2 | @moduledoc false 3 | 4 | def payload do 5 | [ 6 | %{ 7 | language: "elixir", 8 | pid: NewRelic.Util.pid(), 9 | host: NewRelic.Util.hostname(), 10 | display_host: NewRelic.Config.host_display_name(), 11 | app_name: NewRelic.Config.app_name(), 12 | labels: 13 | Enum.map(NewRelic.Config.labels(), fn [key, value] -> 14 | %{label_type: key, label_value: value} 15 | end), 16 | utilization: NewRelic.Util.utilization(), 17 | event_harvest_config: NewRelic.Config.event_harvest_config(), 18 | metadata: NewRelic.Util.metadata(), 19 | environment: NewRelic.Util.elixir_environment(), 20 | agent_version: NewRelic.Config.agent_version() 21 | } 22 | ] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/collector/custom_event/harvester.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.Collector.CustomEvent.Harvester do 2 | use GenServer 3 | 4 | @moduledoc false 5 | 6 | alias NewRelic.Harvest 7 | alias NewRelic.Harvest.Collector 8 | alias NewRelic.Custom.Event 9 | 10 | def start_link(_) do 11 | GenServer.start_link(__MODULE__, []) 12 | end 13 | 14 | def init(_) do 15 | {:ok, 16 | %{ 17 | start_time: System.system_time(), 18 | start_time_mono: System.monotonic_time(), 19 | end_time_mono: nil, 20 | sampling: %{ 21 | reservoir_size: Collector.AgentRun.lookup(:custom_event_reservoir_size, 100), 22 | events_seen: 0 23 | }, 24 | custom_events: [] 25 | }} 26 | end 27 | 28 | # API 29 | 30 | def report_custom_event(type, attributes) when is_map(attributes), 31 | do: 32 | %Event{ 33 | type: type, 34 | attributes: process(attributes), 35 | timestamp: System.system_time(:millisecond) / 1_000 36 | } 37 | |> report_custom_event 38 | 39 | def report_custom_event(%Event{} = event), 40 | do: 41 | Collector.CustomEvent.HarvestCycle 42 | |> Harvest.HarvestCycle.current_harvester() 43 | |> GenServer.cast({:report, event}) 44 | 45 | def gather_harvest, 46 | do: 47 | Collector.CustomEvent.HarvestCycle 48 | |> Harvest.HarvestCycle.current_harvester() 49 | |> GenServer.call(:gather_harvest) 50 | 51 | # Server 52 | 53 | def handle_cast(_late_msg, :completed), do: {:noreply, :completed} 54 | 55 | def handle_cast({:report, event}, state) do 56 | state = 57 | state 58 | |> store_event(event) 59 | |> store_sampling 60 | 61 | {:noreply, state} 62 | end 63 | 64 | def handle_call(_late_msg, _from, :completed), do: {:reply, :completed, :completed} 65 | 66 | def handle_call(:send_harvest, _from, state) do 67 | send_harvest(%{state | end_time_mono: System.monotonic_time()}) 68 | {:reply, :ok, :completed} 69 | end 70 | 71 | def handle_call(:gather_harvest, _from, state) do 72 | {:reply, build_payload(state), state} 73 | end 74 | 75 | # Helpers 76 | 77 | defp process(event) do 78 | event 79 | |> NewRelic.Util.coerce_attributes() 80 | |> Map.merge(NewRelic.Config.automatic_attributes()) 81 | end 82 | 83 | defp store_event(%{sampling: %{events_seen: seen, reservoir_size: size}} = state, event) 84 | when seen < size, 85 | do: %{state | custom_events: [event | state.custom_events]} 86 | 87 | defp store_event(state, _event), do: state 88 | 89 | defp store_sampling(%{sampling: sampling} = state), 90 | do: %{state | sampling: Map.update!(sampling, :events_seen, &(&1 + 1))} 91 | 92 | defp send_harvest(state) do 93 | events = build_payload(state) 94 | Collector.Protocol.custom_event([Collector.AgentRun.agent_run_id(), state.sampling, events]) 95 | log_harvest(length(events), state.sampling.events_seen, state.sampling.reservoir_size) 96 | end 97 | 98 | defp log_harvest(harvest_size, events_seen, reservoir_size) do 99 | NewRelic.report_metric({:supportability, "CustomEventData"}, harvest_size: harvest_size) 100 | 101 | NewRelic.report_metric({:supportability, "CustomEventData"}, 102 | events_seen: events_seen, 103 | reservoir_size: reservoir_size 104 | ) 105 | 106 | NewRelic.log( 107 | :debug, 108 | "Completed Custom Event harvest - " <> 109 | "size: #{harvest_size}, seen: #{events_seen}, max: #{reservoir_size}" 110 | ) 111 | end 112 | 113 | defp build_payload(state), do: Event.format_events(state.custom_events) 114 | end 115 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/collector/error_trace/harvester.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.Collector.ErrorTrace.Harvester do 2 | use GenServer 3 | 4 | @moduledoc false 5 | 6 | alias NewRelic.Harvest 7 | alias NewRelic.Harvest.Collector 8 | alias NewRelic.Error.Trace 9 | 10 | def start_link(_) do 11 | GenServer.start_link(__MODULE__, []) 12 | end 13 | 14 | def init(_) do 15 | {:ok, 16 | %{ 17 | start_time: System.system_time(), 18 | start_time_mono: System.monotonic_time(), 19 | end_time_mono: nil, 20 | error_traces_seen: 0, 21 | error_traces: [] 22 | }} 23 | end 24 | 25 | # API 26 | 27 | def report_error(%Trace{} = trace), 28 | do: 29 | Collector.ErrorTrace.HarvestCycle 30 | |> Harvest.HarvestCycle.current_harvester() 31 | |> GenServer.cast({:report, trace}) 32 | 33 | def gather_harvest, 34 | do: 35 | Collector.ErrorTrace.HarvestCycle 36 | |> Harvest.HarvestCycle.current_harvester() 37 | |> GenServer.call(:gather_harvest) 38 | 39 | # Server 40 | 41 | def handle_cast(_late_msg, :completed), do: {:noreply, :completed} 42 | 43 | def handle_cast({:report, trace}, state) do 44 | state = 45 | state 46 | |> store_error(trace) 47 | 48 | {:noreply, state} 49 | end 50 | 51 | def handle_call(_late_msg, _from, :completed), do: {:reply, :completed, :completed} 52 | 53 | def handle_call(:send_harvest, _from, state) do 54 | send_harvest(%{state | end_time_mono: System.monotonic_time()}) 55 | {:reply, :ok, :completed} 56 | end 57 | 58 | def handle_call(:gather_harvest, _from, state) do 59 | {:reply, build_payload(state), state} 60 | end 61 | 62 | # Helpers 63 | 64 | @reservoir_size 20 65 | 66 | defp store_error(%{error_traces_seen: seen} = state, _trace) 67 | when seen >= @reservoir_size do 68 | %{ 69 | state 70 | | error_traces_seen: state.error_traces_seen + 1 71 | } 72 | end 73 | 74 | defp store_error(state, trace) do 75 | %{ 76 | state 77 | | error_traces_seen: state.error_traces_seen + 1, 78 | error_traces: [trace | state.error_traces] 79 | } 80 | end 81 | 82 | defp send_harvest(state) do 83 | errors = build_payload(state) 84 | Collector.Protocol.error([Collector.AgentRun.agent_run_id(), errors]) 85 | log_harvest(length(errors), state.error_traces_seen) 86 | end 87 | 88 | defp log_harvest(harvest_size, events_seen) do 89 | NewRelic.report_metric({:supportability, "ErrorData"}, harvest_size: harvest_size) 90 | 91 | NewRelic.report_metric({:supportability, "ErrorData"}, 92 | events_seen: events_seen, 93 | reservoir_size: @reservoir_size 94 | ) 95 | 96 | NewRelic.log( 97 | :debug, 98 | "Completed Error Trace harvest - " <> 99 | "size: #{harvest_size}, seen: #{events_seen}, max: #{@reservoir_size}" 100 | ) 101 | end 102 | 103 | defp build_payload(state), do: state.error_traces |> Enum.uniq() |> Trace.format_errors() 104 | end 105 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/collector/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.Collector.Supervisor do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | alias NewRelic.Harvest 7 | alias NewRelic.Harvest.Collector 8 | 9 | def start_link(_) do 10 | Supervisor.start_link(__MODULE__, []) 11 | end 12 | 13 | def init(_) do 14 | children = [ 15 | data_supervisor(Collector.Metric, :data_report_period), 16 | data_supervisor(Collector.TransactionTrace, :data_report_period), 17 | data_supervisor(Collector.ErrorTrace, :data_report_period), 18 | data_supervisor(Collector.TransactionEvent, :transaction_event_harvest_cycle), 19 | data_supervisor(Collector.TransactionErrorEvent, :error_event_harvest_cycle), 20 | data_supervisor(Collector.CustomEvent, :custom_event_harvest_cycle), 21 | data_supervisor(Collector.SpanEvent, :span_event_harvest_cycle) 22 | ] 23 | 24 | Supervisor.init(children, strategy: :one_for_all) 25 | end 26 | 27 | defp data_supervisor(namespace, key) do 28 | Supervisor.child_spec( 29 | {Harvest.DataSupervisor, [namespace: namespace, key: key, lookup_module: Collector.AgentRun]}, 30 | id: make_ref() 31 | ) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/collector/transaction_error_event/harvester.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.Collector.TransactionErrorEvent.Harvester do 2 | use GenServer 3 | 4 | @moduledoc false 5 | 6 | alias NewRelic.Harvest 7 | alias NewRelic.Harvest.Collector 8 | alias NewRelic.Error.Event 9 | 10 | def start_link(_) do 11 | GenServer.start_link(__MODULE__, []) 12 | end 13 | 14 | def init(_) do 15 | {:ok, 16 | %{ 17 | start_time: System.system_time(), 18 | start_time_mono: System.monotonic_time(), 19 | end_time_mono: nil, 20 | sampling: %{ 21 | reservoir_size: Collector.AgentRun.lookup(:error_event_reservoir_size, 10), 22 | events_seen: 0 23 | }, 24 | error_events: [] 25 | }} 26 | end 27 | 28 | # API 29 | 30 | def report_error(%Event{} = event), 31 | do: 32 | Collector.TransactionErrorEvent.HarvestCycle 33 | |> Harvest.HarvestCycle.current_harvester() 34 | |> GenServer.cast({:report, event}) 35 | 36 | def gather_harvest, 37 | do: 38 | Collector.TransactionErrorEvent.HarvestCycle 39 | |> Harvest.HarvestCycle.current_harvester() 40 | |> GenServer.call(:gather_harvest) 41 | 42 | # Server 43 | 44 | def handle_cast(_late_msg, :completed), do: {:noreply, :completed} 45 | 46 | def handle_cast({:report, event}, state) do 47 | state = 48 | state 49 | |> store_event(event) 50 | |> store_sampling 51 | 52 | {:noreply, state} 53 | end 54 | 55 | def handle_call(_late_msg, _from, :completed), do: {:reply, :completed, :completed} 56 | 57 | def handle_call(:send_harvest, _from, state) do 58 | send_harvest(%{state | end_time_mono: System.monotonic_time()}) 59 | {:reply, :ok, :completed} 60 | end 61 | 62 | def handle_call(:gather_harvest, _from, state) do 63 | {:reply, build_payload(state), state} 64 | end 65 | 66 | # Helpers 67 | 68 | defp store_event(%{sampling: %{events_seen: seen, reservoir_size: size}} = state, event) 69 | when seen < size, 70 | do: %{state | error_events: [event | state.error_events]} 71 | 72 | defp store_event(state, _event), do: state 73 | 74 | defp store_sampling(%{sampling: sampling} = state), 75 | do: %{state | sampling: Map.update!(sampling, :events_seen, &(&1 + 1))} 76 | 77 | defp send_harvest(state) do 78 | events = build_payload(state) 79 | Collector.Protocol.error_event([Collector.AgentRun.agent_run_id(), state.sampling, events]) 80 | log_harvest(length(events), state.sampling.events_seen, state.sampling.reservoir_size) 81 | end 82 | 83 | defp log_harvest(harvest_size, events_seen, reservoir_size) do 84 | NewRelic.report_metric({:supportability, "ErrorEventData"}, harvest_size: harvest_size) 85 | 86 | NewRelic.log( 87 | :debug, 88 | "Completed TransactionError Event harvest - " <> 89 | "size: #{harvest_size}, seen: #{events_seen}, max: #{reservoir_size}" 90 | ) 91 | end 92 | 93 | defp build_payload(state), do: Event.format_events(state.error_events) 94 | end 95 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/collector/transaction_event/harvester.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.Collector.TransactionEvent.Harvester do 2 | use GenServer 3 | 4 | @moduledoc false 5 | 6 | alias NewRelic.Harvest 7 | alias NewRelic.Harvest.Collector 8 | alias NewRelic.Transaction.Event 9 | alias NewRelic.Util.PriorityQueue 10 | 11 | def start_link(_) do 12 | GenServer.start_link(__MODULE__, []) 13 | end 14 | 15 | def init(_) do 16 | {:ok, 17 | %{ 18 | start_time: System.system_time(), 19 | start_time_mono: System.monotonic_time(), 20 | end_time_mono: nil, 21 | sampling: %{ 22 | reservoir_size: Collector.AgentRun.lookup(:transaction_event_reservoir_size, 100), 23 | events_seen: 0 24 | }, 25 | events: PriorityQueue.new() 26 | }} 27 | end 28 | 29 | # API 30 | 31 | def report_event(%Event{} = event), 32 | do: 33 | Collector.TransactionEvent.HarvestCycle 34 | |> Harvest.HarvestCycle.current_harvester() 35 | |> GenServer.cast({:report, event}) 36 | 37 | def gather_harvest, 38 | do: 39 | Collector.TransactionEvent.HarvestCycle 40 | |> Harvest.HarvestCycle.current_harvester() 41 | |> GenServer.call(:gather_harvest) 42 | 43 | # Server 44 | 45 | def handle_cast(_late_msg, :completed), do: {:noreply, :completed} 46 | 47 | def handle_cast({:report, event}, state) do 48 | state = 49 | state 50 | |> store_event(event) 51 | |> store_sampling 52 | 53 | {:noreply, state} 54 | end 55 | 56 | def handle_call(_late_msg, _from, :completed), do: {:reply, :completed, :completed} 57 | 58 | def handle_call(:send_harvest, _from, state) do 59 | send_harvest(%{state | end_time_mono: System.monotonic_time()}) 60 | {:reply, :ok, :completed} 61 | end 62 | 63 | def handle_call(:gather_harvest, _from, state) do 64 | {:reply, build_payload(state), state} 65 | end 66 | 67 | # Helpers 68 | 69 | defp store_event(%{sampling: %{reservoir_size: size}} = state, event) do 70 | key = event.user_attributes[:priority] || :rand.uniform() |> Float.round(6) 71 | %{state | events: PriorityQueue.insert(state.events, size, key, event)} 72 | end 73 | 74 | defp store_sampling(%{sampling: sampling} = state), 75 | do: %{state | sampling: Map.update!(sampling, :events_seen, &(&1 + 1))} 76 | 77 | defp send_harvest(state) do 78 | events = build_payload(state) 79 | 80 | Collector.Protocol.transaction_event([ 81 | Collector.AgentRun.agent_run_id(), 82 | state.sampling, 83 | events 84 | ]) 85 | 86 | log_harvest(length(events), state.sampling.events_seen, state.sampling.reservoir_size) 87 | end 88 | 89 | defp log_harvest(harvest_size, events_seen, reservoir_size) do 90 | NewRelic.report_metric({:supportability, "AnalyticEventData"}, harvest_size: harvest_size) 91 | 92 | NewRelic.report_metric({:supportability, "AnalyticEventData"}, 93 | events_seen: events_seen, 94 | reservoir_size: reservoir_size 95 | ) 96 | 97 | NewRelic.log( 98 | :debug, 99 | "Completed Transaction Event harvest - " <> 100 | "size: #{harvest_size}, seen: #{events_seen}, max: #{reservoir_size}" 101 | ) 102 | end 103 | 104 | defp build_payload(state) do 105 | state.events 106 | |> PriorityQueue.values() 107 | |> Event.format_events() 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/data_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.DataSupervisor do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | alias NewRelic.Harvest 7 | 8 | def start_link(config) do 9 | Supervisor.start_link(__MODULE__, config) 10 | end 11 | 12 | def init(namespace: namespace, key: harvest_cycle_key, lookup_module: lookup_module) do 13 | harvester = Module.concat(namespace, Harvester) 14 | harvester_supervisor = Module.concat(namespace, HarvesterSupervisor) 15 | harvester_cycle = Module.concat(namespace, HarvestCycle) 16 | 17 | children = [ 18 | {Harvest.HarvesterSupervisor, harvester: harvester, name: harvester_supervisor}, 19 | {Harvest.HarvestCycle, 20 | name: harvester_cycle, 21 | child_spec: harvester, 22 | harvest_cycle_key: harvest_cycle_key, 23 | supervisor: harvester_supervisor, 24 | lookup_module: lookup_module} 25 | ] 26 | 27 | Supervisor.init(children, strategy: :one_for_one) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/harvester_store.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.HarvesterStore do 2 | use GenServer 3 | 4 | # Wrapper around an ETS table that tracks the current harvesters 5 | 6 | @moduledoc false 7 | 8 | def start_link(_) do 9 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 10 | end 11 | 12 | def init(:ok) do 13 | NewRelic.sample_process() 14 | :ets.new(__MODULE__, [:named_table, :set, :public, read_concurrency: true]) 15 | {:ok, %{}} 16 | end 17 | 18 | def current(harvester) do 19 | case :ets.lookup(__MODULE__, harvester) do 20 | [{^harvester, pid}] -> pid 21 | _ -> nil 22 | end 23 | rescue 24 | _ -> nil 25 | end 26 | 27 | def update(harvester, pid) do 28 | :ets.insert(__MODULE__, {harvester, pid}) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/harvester_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.HarvesterSupervisor do 2 | use DynamicSupervisor 3 | 4 | @moduledoc false 5 | 6 | def start_link(harvester: harvester, name: name) do 7 | DynamicSupervisor.start_link(__MODULE__, harvester, name: name) 8 | end 9 | 10 | def start_child(supervisor, harvester) do 11 | DynamicSupervisor.start_child(supervisor, harvester) 12 | end 13 | 14 | def init(_harvester) do 15 | DynamicSupervisor.init(strategy: :one_for_one, max_restarts: 10) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.Supervisor do 2 | use Supervisor 3 | 4 | alias NewRelic.Harvest 5 | 6 | @moduledoc false 7 | 8 | @all_harvesters [ 9 | Harvest.Collector.Metric.HarvestCycle, 10 | Harvest.Collector.TransactionTrace.HarvestCycle, 11 | Harvest.Collector.TransactionEvent.HarvestCycle, 12 | Harvest.Collector.SpanEvent.HarvestCycle, 13 | Harvest.Collector.TransactionErrorEvent.HarvestCycle, 14 | Harvest.Collector.CustomEvent.HarvestCycle, 15 | Harvest.Collector.ErrorTrace.HarvestCycle, 16 | Harvest.TelemetrySdk.Logs.HarvestCycle, 17 | Harvest.TelemetrySdk.Spans.HarvestCycle, 18 | Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle 19 | ] 20 | 21 | def start_link(_) do 22 | Supervisor.start_link(__MODULE__, []) 23 | end 24 | 25 | def init(_) do 26 | children = [ 27 | {Task.Supervisor, name: Harvest.TaskSupervisor}, 28 | Harvest.Collector.Supervisor, 29 | Harvest.TelemetrySdk.Supervisor 30 | ] 31 | 32 | Supervisor.init(children, strategy: :one_for_one) 33 | end 34 | 35 | def manual_shutdown do 36 | if NewRelic.Config.enabled?() do 37 | @all_harvesters 38 | |> Enum.map( 39 | &Task.async(fn -> 40 | Harvest.HarvestCycle.manual_shutdown(&1) 41 | end) 42 | ) 43 | |> Enum.map(&Task.await/1) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/telemetry_sdk/api.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.TelemetrySdk.API do 2 | @moduledoc false 3 | 4 | def log(logs) do 5 | url = url(:log) 6 | payload = {:logs, logs, generate_request_id()} 7 | 8 | post(url, payload) 9 | |> maybe_retry(url, payload) 10 | end 11 | 12 | def span(spans) do 13 | url = url(:trace) 14 | payload = {:spans, spans, generate_request_id()} 15 | 16 | post(url, payload) 17 | |> maybe_retry(url, payload) 18 | end 19 | 20 | def dimensional_metric(metrics) do 21 | url = url(:metric) 22 | payload = {:metrics, metrics, generate_request_id()} 23 | 24 | post(url, payload) 25 | |> maybe_retry(url, payload) 26 | end 27 | 28 | @success 200..299 29 | @drop [400, 401, 403, 405, 409, 410, 411] 30 | defp maybe_retry({:ok, %{status_code: status_code}} = result, _, _) 31 | when status_code in @success 32 | when status_code in @drop do 33 | result 34 | end 35 | 36 | # 413 split 37 | 38 | # 408, 500+ 39 | defp maybe_retry(_result, url, payload) do 40 | post(url, payload) 41 | end 42 | 43 | defp post(url, {_, payload, request_id}) do 44 | NewRelic.Util.HTTP.post(url, payload, headers(request_id)) 45 | end 46 | 47 | defp url(type) do 48 | NewRelic.Config.get(:telemetry_hosts)[type] 49 | end 50 | 51 | defp headers(request_id) do 52 | [ 53 | "X-Request-Id": request_id, 54 | "X-License-Key": NewRelic.Config.license_key(), 55 | "User-Agent": user_agent() 56 | ] 57 | end 58 | 59 | defp user_agent() do 60 | "NewRelic-Elixir-TelemetrySDK/0.1.0 " <> 61 | "NewRelic-Elixir-Agent/#{NewRelic.Config.agent_version()}" 62 | end 63 | 64 | defp generate_request_id() do 65 | NewRelic.Util.uuid4() 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/telemetry_sdk/config.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.TelemetrySdk.Config do 2 | @moduledoc false 3 | 4 | @default %{ 5 | logs_harvest_cycle: 5_000, 6 | spans_harvest_cycle: 5_000, 7 | dimensional_metrics_harvest_cycle: 5_000 8 | } 9 | def lookup(key) do 10 | Application.get_env(:new_relic_agent, key, @default[key]) 11 | end 12 | 13 | @region_matcher ~r/^(?\D+)/ 14 | @env_matcher ~r/^(?.+)-collector/ 15 | def determine_hosts(host, region) do 16 | env = host && Regex.named_captures(@env_matcher, host)["env"] 17 | env = env && env <> "-" 18 | region = region && Regex.named_captures(@region_matcher, region)["region"] <> "." 19 | 20 | %{ 21 | log: "https://#{env}log-api.#{region}newrelic.com/log/v1", 22 | trace: trace_domain(env, region), 23 | metric: metric_domain(env, region) 24 | } 25 | end 26 | 27 | defp trace_domain(env, region) do 28 | infinite_tracing_host = NewRelic.Init.determine_config(:infinite_tracing_trace_observer_host) 29 | trace_domain(env, region, infinite_tracing_host) 30 | end 31 | 32 | defp trace_domain(env, region, nil) do 33 | "https://#{env}trace-api.#{region}newrelic.com/trace/v1" 34 | end 35 | 36 | defp trace_domain(_env, _region, infinite_tracing_host) do 37 | "https://#{infinite_tracing_host}/trace/v1" 38 | end 39 | 40 | defp metric_domain(env, region) do 41 | "https://#{env}metric-api.#{region}newrelic.com/metric/v1" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/telemetry_sdk/logs/harvester.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.TelemetrySdk.Logs.Harvester do 2 | use GenServer 3 | 4 | @moduledoc false 5 | 6 | alias NewRelic.Harvest 7 | alias NewRelic.Harvest.TelemetrySdk 8 | 9 | def start_link(_) do 10 | GenServer.start_link(__MODULE__, []) 11 | end 12 | 13 | def init(_) do 14 | {:ok, 15 | %{ 16 | start_time: System.system_time(), 17 | start_time_mono: System.monotonic_time(), 18 | end_time_mono: nil, 19 | sampling: %{ 20 | reservoir_size: Application.get_env(:new_relic_agent, :log_reservoir_size, 5_000), 21 | logs_seen: 0 22 | }, 23 | logs: [] 24 | }} 25 | end 26 | 27 | # API 28 | 29 | def report_log(log), 30 | do: 31 | TelemetrySdk.Logs.HarvestCycle 32 | |> Harvest.HarvestCycle.current_harvester() 33 | |> GenServer.cast({:report, log}) 34 | 35 | def gather_harvest, 36 | do: 37 | TelemetrySdk.Logs.HarvestCycle 38 | |> Harvest.HarvestCycle.current_harvester() 39 | |> GenServer.call(:gather_harvest) 40 | 41 | def handle_cast(_late_msg, :completed), do: {:noreply, :completed} 42 | 43 | def handle_cast({:report, log}, state) do 44 | state = 45 | state 46 | |> store_log(log) 47 | |> store_sampling 48 | 49 | {:noreply, state} 50 | end 51 | 52 | def handle_call(_late_msg, _from, :completed), do: {:reply, :completed, :completed} 53 | 54 | def handle_call(:send_harvest, _from, state) do 55 | send_harvest(%{state | end_time_mono: System.monotonic_time()}) 56 | {:reply, :ok, :completed} 57 | end 58 | 59 | def handle_call(:gather_harvest, _from, state) do 60 | {:reply, build_log_data(state.logs), state} 61 | end 62 | 63 | # Helpers 64 | 65 | defp store_log(%{sampling: %{logs_seen: seen, reservoir_size: size}} = state, log) 66 | when seen < size, 67 | do: %{state | logs: [log | state.logs]} 68 | 69 | defp store_log(state, _log), 70 | do: state 71 | 72 | defp store_sampling(%{sampling: sampling} = state), 73 | do: %{state | sampling: Map.update!(sampling, :logs_seen, &(&1 + 1))} 74 | 75 | defp send_harvest(state) do 76 | TelemetrySdk.API.log(build_log_data(state.logs)) 77 | log_harvest(length(state.logs), state.sampling.logs_seen, state.sampling.reservoir_size) 78 | end 79 | 80 | defp log_harvest(harvest_size, logs_seen, reservoir_size) do 81 | NewRelic.log( 82 | :debug, 83 | "Completed TelemetrySdk.Logs harvest - " <> 84 | "size: #{harvest_size}, seen: #{logs_seen}, max: #{reservoir_size}" 85 | ) 86 | end 87 | 88 | defp build_log_data(logs) do 89 | [ 90 | %{ 91 | logs: logs, 92 | common: common() 93 | } 94 | ] 95 | end 96 | 97 | defp common() do 98 | %{ 99 | attributes: NewRelic.LogsInContext.linking_metadata() 100 | } 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/new_relic/harvest/telemetry_sdk/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Harvest.TelemetrySdk.Supervisor do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | alias NewRelic.Harvest 7 | alias NewRelic.Harvest.TelemetrySdk 8 | 9 | def start_link(_) do 10 | Supervisor.start_link(__MODULE__, []) 11 | end 12 | 13 | def init(_) do 14 | children = [ 15 | data_supervisor(TelemetrySdk.Logs, :logs_harvest_cycle), 16 | data_supervisor(TelemetrySdk.Spans, :spans_harvest_cycle), 17 | data_supervisor(TelemetrySdk.DimensionalMetrics, :dimensional_metrics_harvest_cycle) 18 | ] 19 | 20 | Supervisor.init(children, strategy: :one_for_all) 21 | end 22 | 23 | defp data_supervisor(namespace, key) do 24 | Supervisor.child_spec( 25 | {Harvest.DataSupervisor, [namespace: namespace, key: key, lookup_module: TelemetrySdk.Config]}, 26 | id: make_ref() 27 | ) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/new_relic/instrumented/mix/task.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Instrumented.Mix.Task do 2 | defmacro __using__(_args) do 3 | quote do 4 | case Module.get_attribute(__MODULE__, :behaviour) do 5 | [Mix.Task] -> 6 | @before_compile NewRelic.Instrumented.Mix.Task 7 | 8 | _ -> 9 | require Logger 10 | 11 | Logger.error("[New Relic] Unable to instrument #{inspect(__MODULE__)} since it isn't a Mix.Task") 12 | end 13 | end 14 | end 15 | 16 | defmacro __before_compile__(%{module: module}) do 17 | Module.make_overridable(module, run: 1) 18 | 19 | quote do 20 | def run(args) do 21 | Application.ensure_all_started(:new_relic_agent) 22 | NewRelic.Harvest.Collector.AgentRun.ensure_initialized() 23 | 24 | "Elixir.Mix.Tasks." <> task_name = Atom.to_string(__MODULE__) 25 | NewRelic.start_transaction("Mix.Task", task_name) 26 | 27 | super(args) 28 | 29 | NewRelic.stop_transaction() 30 | Application.stop(:new_relic_agent) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/new_relic/instrumented/task.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Instrumented.Task do 2 | @moduledoc """ 3 | Provides a pre-instrumented convienince module to connect 4 | non-linked `Task` processes to the Transaction that called them. 5 | 6 | You may call these functions directly, or `alias` the module 7 | and continue to use `Task` as normal. 8 | 9 | Example usage: 10 | ```elixir 11 | alias NewRelic.Instrumented.Task 12 | 13 | Task.async_stream([1,2], fn n -> do_work(n) end) 14 | ``` 15 | """ 16 | 17 | import NewRelic.Instrumented.Task.Wrappers 18 | 19 | defdelegate async(fun), 20 | to: Task 21 | 22 | defdelegate async(module, function_name, args), 23 | to: Task 24 | 25 | defdelegate await(task, timeout \\ 5000), 26 | to: Task 27 | 28 | if Code.ensure_loaded?(Task) && Kernel.function_exported?(Task, :await_many, 2) do 29 | defdelegate await_many(tasks, timeout \\ 5000), 30 | to: Task 31 | end 32 | 33 | defdelegate child_spec(arg), 34 | to: Task 35 | 36 | defdelegate shutdown(task, timeout \\ 5000), 37 | to: Task 38 | 39 | defdelegate start_link(fun), 40 | to: Task 41 | 42 | defdelegate start_link(module, function_name, args), 43 | to: Task 44 | 45 | defdelegate yield(task, timeout \\ 5000), 46 | to: Task 47 | 48 | defdelegate yield_many(task, timeout \\ 5000), 49 | to: Task 50 | 51 | # These functions _don't_ link their Task so we connect them explicitly 52 | 53 | def start(fun) do 54 | Task.start(instrument(fun)) 55 | end 56 | 57 | def start(module, function_name, args) do 58 | {module, function_name, args} = instrument({module, function_name, args}) 59 | Task.start(module, function_name, args) 60 | end 61 | 62 | def async_stream(enumerable, fun, options \\ []) do 63 | Task.async_stream(enumerable, instrument(fun), options) 64 | end 65 | 66 | def async_stream(enumerable, module, function_name, args, options \\ []) do 67 | {module, function_name, args} = instrument({module, function_name, args}) 68 | Task.async_stream(enumerable, module, function_name, args, options) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/new_relic/instrumented/task/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Instrumented.Task.Supervisor do 2 | @moduledoc """ 3 | Provides a pre-instrumented convienince module to connect 4 | non-linked `Task.Supervisor` processes to the Transaction 5 | that called them. 6 | 7 | You may call these functions directly, or `alias` the 8 | `NewRelic.Instrumented.Task` module and continue to use 9 | `Task` as normal. 10 | 11 | Example usage: 12 | ```elixir 13 | alias NewRelic.Instrumented.Task 14 | 15 | Task.Supervisor.async_nolink( 16 | MySupervisor, 17 | [1,2], 18 | fn n -> do_work(n) end 19 | ) 20 | ``` 21 | """ 22 | 23 | import NewRelic.Instrumented.Task.Wrappers 24 | 25 | defdelegate async(supervisor, fun, options \\ []), 26 | to: Task.Supervisor 27 | 28 | defdelegate async(supervisor, module, fun, args, options \\ []), 29 | to: Task.Supervisor 30 | 31 | defdelegate children(supervisor), 32 | to: Task.Supervisor 33 | 34 | defdelegate start_link(options), 35 | to: Task.Supervisor 36 | 37 | defdelegate terminate_child(supervisor, pid), 38 | to: Task.Supervisor 39 | 40 | # These functions _don't_ link their Task so we connect them explicitly 41 | 42 | def async_stream(supervisor, enumerable, fun, options \\ []) do 43 | Task.Supervisor.async_stream(supervisor, enumerable, instrument(fun), options) 44 | end 45 | 46 | def async_stream(supervisor, enumerable, module, function, args, options \\ []) do 47 | {module, function, args} = instrument({module, function, args}) 48 | Task.Supervisor.async_stream(supervisor, enumerable, module, function, args, options) 49 | end 50 | 51 | def async_nolink(supervisor, fun, options \\ []) do 52 | Task.Supervisor.async_nolink(supervisor, instrument(fun), options) 53 | end 54 | 55 | def async_nolink(supervisor, module, fun, args, options \\ []) do 56 | {module, fun, args} = instrument({module, fun, args}) 57 | Task.Supervisor.async_nolink(supervisor, module, fun, args, options) 58 | end 59 | 60 | def async_stream_nolink(supervisor, enumerable, fun, options \\ []) do 61 | Task.Supervisor.async_stream_nolink(supervisor, enumerable, instrument(fun), options) 62 | end 63 | 64 | def async_stream_nolink(supervisor, enumerable, module, function, args, options \\ []) do 65 | {module, function, args} = instrument({module, function, args}) 66 | Task.Supervisor.async_stream_nolink(supervisor, enumerable, module, function, args, options) 67 | end 68 | 69 | def start_child(supervisor, fun, options \\ []) do 70 | Task.Supervisor.start_child(supervisor, instrument(fun), options) 71 | end 72 | 73 | def start_child(supervisor, module, fun, args, options \\ []) do 74 | {module, fun, args} = instrument({module, fun, args}) 75 | Task.Supervisor.start_child(supervisor, module, fun, args, options) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/new_relic/instrumented/task/wrappers.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Instrumented.Task.Wrappers do 2 | @moduledoc false 3 | 4 | def instrument(fun) when is_function(fun, 0) do 5 | tx = NewRelic.get_transaction() 6 | 7 | fn -> 8 | NewRelic.connect_to_transaction(tx) 9 | fun.() 10 | end 11 | end 12 | 13 | def instrument(fun) when is_function(fun, 1) do 14 | tx = NewRelic.get_transaction() 15 | 16 | fn val -> 17 | NewRelic.connect_to_transaction(tx) 18 | fun.(val) 19 | end 20 | end 21 | 22 | def instrument({module, fun, args}) do 23 | {__MODULE__, :instrument_mfa, [NewRelic.get_transaction(), {module, fun, args}]} 24 | end 25 | 26 | def instrument_mfa(tx, {module, fun, args}) do 27 | Process.put(:"$initial_call", {module, fun, args}) 28 | NewRelic.connect_to_transaction(tx) 29 | apply(module, fun, args) 30 | end 31 | 32 | def instrument_mfa(val, tx, {module, fun, args}) do 33 | instrument_mfa(tx, {module, fun, [val | args]}) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/new_relic/json.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.JSON do 2 | @moduledoc false 3 | 4 | cond do 5 | Code.ensure_loaded?(JSON) -> 6 | def decode(data), do: apply(JSON, :decode, [data]) 7 | def decode!(data), do: apply(JSON, :decode!, [data]) 8 | def encode!(data), do: apply(JSON, :encode!, [data]) 9 | 10 | Code.ensure_loaded?(Jason) -> 11 | def decode(data), do: apply(Jason, :decode, [data]) 12 | def decode!(data), do: apply(Jason, :decode!, [data]) 13 | def encode!(data), do: apply(Jason, :encode!, [data]) 14 | 15 | true -> 16 | raise "[:new_relic_agent] No JSON library found, please add :jason as a dependency" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/new_relic/logger.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Logger do 2 | use GenServer 3 | require Logger 4 | 5 | # Log Agent events to the configured output device 6 | # - "tmp/new_relic.log" (Default) 7 | # - :memory 8 | # - :stdio 9 | # - {:file, filename} 10 | 11 | @moduledoc false 12 | 13 | def start_link(_) do 14 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 15 | end 16 | 17 | def init(:ok) do 18 | NewRelic.sample_process() 19 | logger = initial_logger() 20 | {:ok, io_device} = device(logger) 21 | {:ok, %{io_device: io_device, logger: logger}} 22 | end 23 | 24 | # API 25 | 26 | @levels [:debug, :info, :warning, :error] 27 | def log(level, message) when level in @levels do 28 | GenServer.cast(__MODULE__, {:log, level, message}) 29 | end 30 | 31 | # Server 32 | 33 | def handle_cast({:log, level, message}, %{io_device: Logger} = state) do 34 | elixir_logger(level, "new_relic_agent - " <> message) 35 | {:noreply, state} 36 | end 37 | 38 | def handle_cast({:log, level, message}, %{io_device: io_device} = state) do 39 | IO.write(io_device, formatted(level, message)) 40 | {:noreply, state} 41 | end 42 | 43 | def handle_call(:flush, _from, %{logger: :memory, io_device: io_device} = state) do 44 | {:reply, StringIO.flush(io_device), state} 45 | end 46 | 47 | def handle_call(:flush, _from, state) do 48 | {:reply, "", state} 49 | end 50 | 51 | def handle_call({:logger, logger}, _from, old_state) do 52 | {:ok, io_device} = device(logger) 53 | {:reply, old_state, %{io_device: io_device, logger: logger}} 54 | end 55 | 56 | def handle_call({:replace, logger}, _from, _old_state) do 57 | {:reply, :ok, logger} 58 | end 59 | 60 | # Helpers 61 | 62 | def initial_logger do 63 | case NewRelic.Config.logger() do 64 | nil -> :logger 65 | "file" -> {:file, "tmp/new_relic.log"} 66 | "stdout" -> :stdio 67 | "memory" -> :memory 68 | "Logger" -> :logger 69 | log_file_path -> {:file, log_file_path} 70 | end 71 | end 72 | 73 | defp device(:stdio), do: {:ok, :stdio} 74 | defp device(:memory), do: StringIO.open("") 75 | defp device(:logger), do: {:ok, Logger} 76 | 77 | defp device({:file, logfile}) do 78 | log(:info, "Log File: #{Path.absname(logfile)}") 79 | logfile |> Path.dirname() |> File.mkdir_p!() 80 | {:ok, _file} = File.open(logfile, [:append, :utf8]) 81 | end 82 | 83 | defp elixir_logger(:debug, message), do: Logger.debug(message) 84 | defp elixir_logger(:info, message), do: Logger.info(message) 85 | defp elixir_logger(:warning, message), do: Logger.warning(message) 86 | defp elixir_logger(:error, message), do: Logger.error(message) 87 | 88 | @sep " - " 89 | defp formatted(level, message), do: [formatted(level), @sep, timestamp(), @sep, message, "\n"] 90 | defp formatted(:debug), do: "[DEBUG]" 91 | defp formatted(:info), do: "[INFO]" 92 | defp formatted(:warning), do: "[WARN]" 93 | defp formatted(:error), do: "[ERROR]" 94 | 95 | defp timestamp do 96 | :calendar.local_time() 97 | |> NaiveDateTime.from_erl!() 98 | |> NaiveDateTime.to_string() 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/new_relic/logs_in_context/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.LogsInContext.Supervisor do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | def start_link(_) do 7 | Supervisor.start_link(__MODULE__, []) 8 | end 9 | 10 | def init(_) do 11 | mode = NewRelic.Config.feature(:logs_in_context) 12 | NewRelic.LogsInContext.configure(mode) 13 | 14 | Supervisor.init([], strategy: :one_for_one) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/new_relic/metric/metric.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Metric do 2 | @moduledoc false 3 | 4 | defstruct name: "", 5 | scope: "", 6 | call_count: 0, 7 | total_call_time: 0, 8 | total_exclusive_time: 0, 9 | min_call_time: 0, 10 | max_call_time: 0, 11 | sum_of_squares: 0 12 | end 13 | -------------------------------------------------------------------------------- /lib/new_relic/os_mon.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.OsMon do 2 | def start() do 3 | Application.ensure_all_started(:os_mon) 4 | :persistent_term.put(__MODULE__, true) 5 | end 6 | 7 | @mb 1024 * 1024 8 | def get_system_memory() do 9 | when_enabled(fn -> 10 | case :memsup.get_system_memory_data()[:system_total_memory] do 11 | nil -> nil 12 | bytes -> trunc(bytes / @mb) 13 | end 14 | end) 15 | end 16 | 17 | def util() do 18 | when_enabled(fn -> 19 | :cpu_sup.util() 20 | end) 21 | end 22 | 23 | defp when_enabled(fun, default \\ nil) do 24 | case :persistent_term.get(__MODULE__, false) do 25 | true -> fun.() 26 | false -> default 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/new_relic/other_transaction.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.OtherTransaction do 2 | @moduledoc false 3 | 4 | def start_transaction(category, name, headers \\ %{}) do 5 | NewRelic.Transaction.Reporter.start_transaction(:other) 6 | NewRelic.DistributedTrace.start(:other, headers) 7 | 8 | NewRelic.add_attributes( 9 | pid: inspect(self()), 10 | other_transaction_name: "#{category}/#{name}" 11 | ) 12 | 13 | :ok 14 | end 15 | 16 | def stop_transaction() do 17 | NewRelic.Transaction.Reporter.stop_transaction(:other) 18 | :ok 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/new_relic/sampler/agent.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Sampler.Agent do 2 | use GenServer 3 | 4 | # Takes samples of the state of the Agent 5 | 6 | @moduledoc false 7 | 8 | def start_link(_) do 9 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 10 | end 11 | 12 | def init(:ok) do 13 | if NewRelic.Config.enabled?(), 14 | do: Process.send_after(self(), :report, NewRelic.Sampler.Reporter.random_sample_offset()) 15 | 16 | {:ok, %{}} 17 | end 18 | 19 | def handle_info(:report, state) do 20 | record_sample() 21 | Process.send_after(self(), :report, NewRelic.Sampler.Reporter.sample_cycle()) 22 | {:noreply, state} 23 | end 24 | 25 | def handle_call(:report, _from, state) do 26 | record_sample() 27 | {:reply, :ok, state} 28 | end 29 | 30 | defp record_sample do 31 | NewRelic.report_metric( 32 | {:supportability, :agent, "Sidecar/Process/ActiveCount"}, 33 | value: NewRelic.Transaction.Sidecar.counter() 34 | ) 35 | 36 | NewRelic.report_metric( 37 | {:supportability, :agent, "Sidecar/Stores/ContextStore/Size"}, 38 | value: ets_size(NewRelic.Transaction.Sidecar.ContextStore) 39 | ) 40 | 41 | NewRelic.report_metric( 42 | {:supportability, :agent, "Sidecar/Stores/LookupStore/Size"}, 43 | value: ets_size(NewRelic.Transaction.Sidecar.LookupStore) 44 | ) 45 | 46 | NewRelic.report_metric( 47 | {:supportability, :agent, "ErlangTrace/Restarts"}, 48 | value: NewRelic.Transaction.ErlangTraceManager.restart_count() 49 | ) 50 | end 51 | 52 | defp ets_size(table) do 53 | :ets.info(table, :size) 54 | rescue 55 | ArgumentError -> nil 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/new_relic/sampler/ets.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Sampler.Ets do 2 | use GenServer 3 | @kb 1024 4 | @word_size :erlang.system_info(:wordsize) 5 | 6 | # Takes samples of the state of ETS tables 7 | 8 | @moduledoc false 9 | 10 | def start_link(_) do 11 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 12 | end 13 | 14 | def init(:ok) do 15 | NewRelic.sample_process() 16 | 17 | if NewRelic.Config.enabled?(), 18 | do: Process.send_after(self(), :report, NewRelic.Sampler.Reporter.random_sample_offset()) 19 | 20 | {:ok, %{}} 21 | end 22 | 23 | def handle_info(:report, state) do 24 | record_sample() 25 | Process.send_after(self(), :report, NewRelic.Sampler.Reporter.sample_cycle()) 26 | {:noreply, state} 27 | end 28 | 29 | def handle_call(:report, _from, state) do 30 | record_sample() 31 | {:reply, :ok, state} 32 | end 33 | 34 | defp record_sample, do: Enum.map(named_tables(), &record_sample/1) 35 | 36 | @size_threshold 500 37 | def record_sample(table) do 38 | case take_sample(table) do 39 | :undefined -> :ignore 40 | %{size: size} when size < @size_threshold -> :ignore 41 | stat -> NewRelic.report_sample(:EtsStat, stat) 42 | end 43 | end 44 | 45 | defp named_tables, do: Enum.reject(:ets.all(), &is_reference/1) 46 | 47 | defp take_sample(table) do 48 | with words when is_number(words) <- :ets.info(table, :memory), 49 | size when is_number(size) <- :ets.info(table, :size) do 50 | %{table_name: inspect(table), memory_kb: round(words * @word_size) / @kb, size: size} 51 | else 52 | :undefined -> :undefined 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/new_relic/sampler/process.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Sampler.Process do 2 | use GenServer 3 | 4 | # Takes samples of the state of requested processes at an interval 5 | 6 | @moduledoc false 7 | 8 | def start_link(_) do 9 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 10 | end 11 | 12 | def init(:ok) do 13 | NewRelic.sample_process() 14 | 15 | if NewRelic.Config.enabled?(), 16 | do: Process.send_after(self(), :report, NewRelic.Sampler.Reporter.random_sample_offset()) 17 | 18 | {:ok, %{pids: %{}, previous: %{}}} 19 | end 20 | 21 | def sample_process, do: GenServer.cast(__MODULE__, {:sample_process, self()}) 22 | 23 | def handle_cast({:sample_process, pid}, state) do 24 | state = store_pid(state.pids[pid], state, pid) 25 | {:noreply, state} 26 | end 27 | 28 | def handle_info(:report, state) do 29 | previous = record_samples(state) 30 | Process.send_after(self(), :report, NewRelic.Sampler.Reporter.sample_cycle()) 31 | {:noreply, %{state | previous: previous}} 32 | end 33 | 34 | def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do 35 | state = %{ 36 | state 37 | | pids: Map.delete(state.pids, pid), 38 | previous: Map.delete(state.previous, pid) 39 | } 40 | 41 | {:noreply, state} 42 | end 43 | 44 | def handle_call(:report, _from, state) do 45 | previous = record_samples(state) 46 | {:reply, :ok, %{state | previous: previous}} 47 | end 48 | 49 | defp record_samples(state) do 50 | Map.new(state.pids, fn {pid, true} -> 51 | {current_sample, stats} = collect(pid, state.previous[pid]) 52 | NewRelic.report_sample(:ProcessSample, stats) 53 | {pid, current_sample} 54 | end) 55 | end 56 | 57 | defp store_pid(true, state, _existing_pid), do: state 58 | 59 | defp store_pid(nil, state, pid) do 60 | Process.monitor(pid) 61 | pids = Map.put(state.pids, pid, true) 62 | previous = Map.put(state.previous, pid, take_sample(pid)) 63 | %{state | pids: pids, previous: previous} 64 | end 65 | 66 | defp collect(pid, previous) do 67 | current_sample = take_sample(pid) 68 | stats = Map.merge(current_sample, delta(previous, current_sample)) 69 | {current_sample, stats} 70 | end 71 | 72 | defp take_sample(pid) do 73 | # http://erlang.org/doc/man/erlang.html#process_info-2 74 | info = Process.info(pid, [:message_queue_len, :memory, :reductions, :registered_name]) 75 | 76 | %{ 77 | pid: inspect(pid), 78 | memory_kb: kb(info[:memory]), 79 | message_queue_length: info[:message_queue_len], 80 | name: parse(:name, info[:registered_name]) || inspect(pid), 81 | reductions: info[:reductions] 82 | } 83 | end 84 | 85 | @kb 1024 86 | defp kb(nil), do: nil 87 | defp kb(bytes), do: bytes / @kb 88 | 89 | defp delta(%{reductions: nil}, _), do: nil 90 | defp delta(_, %{reductions: nil}), do: nil 91 | defp delta(%{reductions: prev}, %{reductions: curr}), do: %{reductions: curr - prev} 92 | 93 | defp parse(:name, []), do: nil 94 | defp parse(:name, name), do: inspect(name) 95 | end 96 | -------------------------------------------------------------------------------- /lib/new_relic/sampler/reporter.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Sampler.Reporter do 2 | @moduledoc false 3 | 4 | def report_sample(category, sample) when is_map(sample), 5 | do: NewRelic.report_custom_event(sampler_event_type(), Map.put(sample, :category, category)) 6 | 7 | defp sampler_event_type, 8 | do: Application.get_env(:new_relic_agent, :sample_event_type, "ElixirSample") 9 | 10 | def sample_cycle, do: Application.get_env(:new_relic_agent, :sample_cycle, 15_000) 11 | 12 | def random_sample_offset, do: :rand.uniform(sample_cycle()) 13 | end 14 | -------------------------------------------------------------------------------- /lib/new_relic/sampler/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Sampler.Supervisor do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | def start_link(_) do 7 | Supervisor.start_link(__MODULE__, []) 8 | end 9 | 10 | def init(_) do 11 | children = [ 12 | NewRelic.Sampler.Agent, 13 | NewRelic.Sampler.Beam, 14 | NewRelic.Sampler.Process, 15 | NewRelic.Sampler.TopProcess, 16 | NewRelic.Sampler.Ets 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/new_relic/sampler/top_process.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Sampler.TopProcess do 2 | use GenServer 3 | 4 | # Track and sample the top processes by: 5 | # * memory usage 6 | # * message queue length 7 | 8 | @moduledoc false 9 | 10 | alias NewRelic.Util.PriorityQueue, as: PQ 11 | 12 | def start_link(_) do 13 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 14 | end 15 | 16 | def init(:ok) do 17 | NewRelic.sample_process() 18 | 19 | if NewRelic.Config.enabled?(), 20 | do: Process.send_after(self(), :sample, NewRelic.Sampler.Reporter.random_sample_offset()) 21 | 22 | {:ok, :reset} 23 | end 24 | 25 | def handle_info(:sample, :reset) do 26 | top_procs = detect_top_processes() 27 | Process.send_after(self(), :report, NewRelic.Sampler.Reporter.sample_cycle()) 28 | {:noreply, top_procs} 29 | end 30 | 31 | @kb 1024 32 | def handle_info(:report, top_procs) do 33 | Enum.each(top_procs, &report_sample/1) 34 | send(self(), :sample) 35 | {:noreply, :reset} 36 | end 37 | 38 | def detect_top_processes() do 39 | {mem_pq, msg_pq} = 40 | Process.list() 41 | |> Enum.reduce({PQ.new(), PQ.new()}, &measure_and_insert/2) 42 | 43 | (PQ.values(mem_pq) ++ PQ.values(msg_pq)) 44 | |> Enum.uniq_by(&elem(&1, 0)) 45 | end 46 | 47 | @size 5 48 | defp measure_and_insert(pid, {mem_pq, msg_pq}) do 49 | case Process.info(pid, [:memory, :message_queue_len, :registered_name, :reductions]) do 50 | [memory: mem, message_queue_len: msg, registered_name: _, reductions: _] = info -> 51 | mem_pq = PQ.insert(mem_pq, @size, mem, {pid, info}) 52 | msg_pq = if msg > 0, do: PQ.insert(msg_pq, @size, msg, {pid, info}), else: msg_pq 53 | {mem_pq, msg_pq} 54 | 55 | nil -> 56 | {mem_pq, msg_pq} 57 | end 58 | end 59 | 60 | defp report_sample({pid, info}) do 61 | case Process.info(pid, :reductions) do 62 | {:reductions, current_reductions} -> 63 | NewRelic.report_sample(:ProcessSample, %{ 64 | pid: inspect(pid), 65 | memory_kb: info[:memory] / @kb, 66 | message_queue_length: info[:message_queue_len], 67 | name: parse(:name, pid, info[:registered_name]), 68 | reductions: current_reductions - info[:reductions] 69 | }) 70 | 71 | nil -> 72 | :ignore 73 | end 74 | end 75 | 76 | defp parse(:name, pid, []) do 77 | with {:dictionary, dictionary} <- Process.info(pid, :dictionary), 78 | {m, f, a} <- Keyword.get(dictionary, :"$initial_call") do 79 | "#{inspect(m)}.#{f}/#{a}" 80 | else 81 | _ -> inspect(pid) 82 | end 83 | end 84 | 85 | defp parse(:name, _pid, name), do: inspect(name) 86 | end 87 | -------------------------------------------------------------------------------- /lib/new_relic/signal_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.SignalHandler do 2 | @moduledoc false 3 | @behaviour :gen_event 4 | 5 | # This signal handler exists so that we can shut down 6 | # the NewRelic.Sampler.Beam process asap to avoid a race 7 | # condition that can happen while `cpu_sup` shuts down 8 | 9 | def start do 10 | case Process.whereis(:erl_signal_server) do 11 | pid when is_pid(pid) -> 12 | # Get our signal handler installed before erlang's 13 | :gen_event.delete_handler(:erl_signal_server, :erl_signal_handler, :ok) 14 | :gen_event.add_handler(:erl_signal_server, __MODULE__, []) 15 | :gen_event.add_handler(:erl_signal_server, :erl_signal_handler, []) 16 | 17 | _ -> 18 | :ok 19 | end 20 | end 21 | 22 | def init(_) do 23 | {:ok, %{}} 24 | end 25 | 26 | def handle_event(:sigterm, state) do 27 | Process.whereis(NewRelic.Sampler.Beam) && 28 | GenServer.stop(NewRelic.Sampler.Beam) 29 | 30 | {:ok, state} 31 | end 32 | 33 | def handle_event(_, state) do 34 | {:ok, state} 35 | end 36 | 37 | def handle_call(_, state) do 38 | {:ok, :ok, state} 39 | end 40 | 41 | def handle_info(_, state) do 42 | {:ok, state} 43 | end 44 | 45 | def terminate(_reason, _state) do 46 | :ok 47 | end 48 | 49 | def code_change(_old, state, _extra) do 50 | {:ok, state} 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/new_relic/span/event.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Span.Event do 2 | # Struct for a Span Event 3 | # 4 | # * trace_id: Distributed Trace ID 5 | # - trace_id from Transaction 6 | # * guid: Segment Identifier 7 | # * parent_id: Segment's Parent's GUID 8 | # - id of incoming DT payload OR `guid` of parent segment 9 | # * transaction_id: Transaction's guid 10 | # * timestamp: Segment start in unix timestamp milliseconds 11 | # * duration: Segment elapsed in seconds 12 | # * category: http | datastore | generic 13 | # * entry_point: Only included if span is First segment 14 | 15 | defstruct type: "Span", 16 | trace_id: nil, 17 | guid: nil, 18 | parent_id: nil, 19 | transaction_id: nil, 20 | sampled: nil, 21 | priority: nil, 22 | timestamp: nil, 23 | duration: nil, 24 | name: nil, 25 | category: nil, 26 | entry_point: false, 27 | category_attributes: %{} 28 | 29 | @moduledoc false 30 | 31 | def format_events(spans) do 32 | Enum.map(spans, &format_event/1) 33 | end 34 | 35 | defp format_event(%__MODULE__{} = span) do 36 | intrinsics = 37 | %{ 38 | type: span.type, 39 | traceId: span.trace_id, 40 | guid: span.guid, 41 | parentId: span.parent_id, 42 | transactionId: span.transaction_id, 43 | sampled: span.sampled, 44 | priority: span.priority, 45 | timestamp: span.timestamp, 46 | duration: span.duration, 47 | name: span.name, 48 | category: span.category 49 | } 50 | |> merge_category_attributes(span.category_attributes) 51 | 52 | intrinsics = 53 | case span.entry_point do 54 | true -> Map.merge(intrinsics, %{"nr.entryPoint": true}) 55 | false -> intrinsics 56 | end 57 | 58 | [ 59 | intrinsics, 60 | _user = %{}, 61 | _agent = %{} 62 | ] 63 | end 64 | 65 | def merge_category_attributes(%{category: "http"} = span, category_attributes) do 66 | {category, custom} = Map.split(category_attributes, [:url, :method, :component]) 67 | 68 | span 69 | |> Map.merge(%{ 70 | "http.url": category[:url] || "url", 71 | "http.method": category[:method] || "method", 72 | component: category[:component] || "component", 73 | "span.kind": "client" 74 | }) 75 | |> Map.merge(custom) 76 | |> NewRelic.Util.coerce_attributes() 77 | end 78 | 79 | def merge_category_attributes(%{category: "datastore"} = span, category_attributes) do 80 | {category, custom} = 81 | Map.split(category_attributes, [:statement, :instance, :address, :hostname, :component]) 82 | 83 | span 84 | |> Map.merge(%{ 85 | "db.statement": category[:statement] || "statement", 86 | "db.instance": category[:instance] || "instance", 87 | "peer.address": category[:address] || "address", 88 | "peer.hostname": category[:hostname] || "hostname", 89 | component: category[:component] || "component", 90 | "span.kind": "client" 91 | }) 92 | |> Map.merge(custom) 93 | |> NewRelic.Util.coerce_attributes() 94 | end 95 | 96 | def merge_category_attributes(span, category_attributes), 97 | do: 98 | Map.merge( 99 | span, 100 | NewRelic.Util.coerce_attributes(category_attributes), 101 | # Don't overwrite existing span keys with custom values 102 | fn _k, v1, _v2 -> v1 end 103 | ) 104 | end 105 | -------------------------------------------------------------------------------- /lib/new_relic/span/reporter.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Span.Reporter do 2 | @moduledoc false 3 | 4 | def report_span(span) do 5 | case NewRelic.Config.feature?(:distributed_tracing) && 6 | NewRelic.Config.feature(:infinite_tracing) do 7 | false -> :ignore 8 | :sampling -> NewRelic.Harvest.Collector.SpanEvent.Harvester.report_span(span) 9 | :infinite -> NewRelic.Harvest.TelemetrySdk.Spans.Harvester.report_span(span) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/new_relic/telemetry/ecto.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Telemetry.Ecto do 2 | use GenServer 3 | 4 | @moduledoc """ 5 | Provides `Ecto` instrumentation via `telemetry`. 6 | 7 | Repos are auto-discovered and instrumented. Make sure your Ecto app depends 8 | on `new_relic_agent` so that the agent can detect when your Repos start. 9 | 10 | We automatically gather: 11 | 12 | * Datastore metrics 13 | * Transaction Trace segments 14 | * Transaction datastore attributes 15 | * Distributed Trace span events 16 | 17 | You can opt-out of this instrumentation as a whole with `:ecto_instrumentation_enabled` 18 | and specifically of query collection with `:query_collection_enabled` via configuration. 19 | See `NewRelic.Config` for details. 20 | """ 21 | 22 | @doc false 23 | def start_link(repo: repo, opts: opts) do 24 | config = %{ 25 | enabled?: NewRelic.Config.feature?(:ecto_instrumentation), 26 | collect_db_query?: NewRelic.Config.feature?(:query_collection), 27 | handler_id: {:new_relic_ecto, repo}, 28 | event: opts[:telemetry_prefix] ++ [:query], 29 | opts: opts 30 | } 31 | 32 | GenServer.start_link(__MODULE__, config) 33 | end 34 | 35 | @doc false 36 | def init(%{enabled?: false}), do: :ignore 37 | 38 | def init(%{enabled?: true} = config) do 39 | :telemetry.attach( 40 | config.handler_id, 41 | config.event, 42 | &NewRelic.Telemetry.Ecto.Handler.handle_event/4, 43 | config 44 | ) 45 | 46 | Process.flag(:trap_exit, true) 47 | {:ok, config} 48 | end 49 | 50 | def terminate(_reason, %{handler_id: handler_id}) do 51 | :telemetry.detach(handler_id) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/new_relic/telemetry/ecto/metadata.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Telemetry.Ecto.Metadata do 2 | @moduledoc false 3 | 4 | def parse(%{result: {:ok, %{__struct__: Postgrex.Cursor}}}), do: :ignore 5 | def parse(%{result: {:ok, %{__struct__: MyXQL.Cursor}}}), do: :ignore 6 | 7 | def parse(%{query: query, result: {_ok_err, %{__struct__: struct}}}) 8 | when struct in [Postgrex.Result, Postgrex.Error] do 9 | {"Postgres", parse_query(query)} 10 | end 11 | 12 | def parse(%{query: query, result: {_ok_err, %{__struct__: struct}}}) 13 | when struct in [MyXQL.Result, MyXQL.Error] do 14 | {"MySQL", parse_query(query)} 15 | end 16 | 17 | def parse(%{query: query, repo: repo, result: {_ok_err, %{__struct__: _struct}}}) do 18 | [adaapter | _] = repo.__adapter__() |> Module.split() |> Enum.reverse() 19 | {adaapter, parse_query(query)} 20 | end 21 | 22 | def parse(%{result: {:ok, _}}), do: :ignore 23 | def parse(%{result: {:error, _}}), do: :ignore 24 | 25 | def parse_query(query) do 26 | case query do 27 | "SELECT" <> _ -> parse_query(:select, query) 28 | "INSERT" <> _ -> parse_query(:insert, query) 29 | "UPDATE" <> _ -> parse_query(:update, query) 30 | "DELETE" <> _ -> parse_query(:delete, query) 31 | "CREATE TABLE" <> _ -> parse_query(:create, query) 32 | "begin" -> {:begin, :other} 33 | "commit" -> {:commit, :other} 34 | "rollback" -> {:rollback, :other} 35 | _ -> {:other, :other} 36 | end 37 | end 38 | 39 | # Table name escaping 40 | # Postgrex: "table" 41 | # MyXQL: `table` 42 | # Tds: [table] 43 | # Exqlite: table 44 | @esc ~w(" ` [ ]) 45 | 46 | @capture %{ 47 | select: ~r/FROM (?\S+)/, 48 | insert: ~r/INSERT INTO (?
\S+)/, 49 | update: ~r/UPDATE (?
\S+)/, 50 | delete: ~r/FROM (?
\S+)/, 51 | create: ~r/CREATE TABLE( IF NOT EXISTS)? (?
\S+)/ 52 | } 53 | def parse_query(operation, query) do 54 | case Regex.named_captures(@capture[operation], query) do 55 | %{"table" => table} -> {operation, String.replace(table, @esc, "")} 56 | _ -> {operation, :other} 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/new_relic/telemetry/ecto/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Telemetry.Ecto.Supervisor do 2 | @moduledoc false 3 | 4 | use DynamicSupervisor 5 | 6 | @ecto_repo_init [:ecto, :repo, :init] 7 | 8 | def start_link(_) do 9 | DynamicSupervisor.start_link(__MODULE__, :ok, name: __MODULE__) 10 | end 11 | 12 | def init(:ok) do 13 | :telemetry.attach( 14 | {:new_relic_ecto, :supervisor}, 15 | @ecto_repo_init, 16 | &__MODULE__.handle_event/4, 17 | %{} 18 | ) 19 | 20 | DynamicSupervisor.init(strategy: :one_for_one) 21 | end 22 | 23 | def handle_event(@ecto_repo_init, _, %{repo: repo, opts: opts}, _) do 24 | NewRelic.log(:info, "Detected Ecto Repo `#{inspect(repo)}`") 25 | 26 | DynamicSupervisor.start_child( 27 | __MODULE__, 28 | {NewRelic.Telemetry.Ecto, [repo: repo, opts: opts]} 29 | ) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/new_relic/telemetry/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Telemetry.Supervisor do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | def start_link(_) do 7 | Supervisor.start_link(__MODULE__, []) 8 | end 9 | 10 | def init(_) do 11 | Supervisor.init( 12 | children(enabled: NewRelic.Config.enabled?()), 13 | strategy: :one_for_one 14 | ) 15 | end 16 | 17 | defp children(enabled: true) do 18 | [ 19 | NewRelic.Telemetry.Ecto.Supervisor, 20 | NewRelic.Telemetry.Redix, 21 | NewRelic.Telemetry.Plug, 22 | NewRelic.Telemetry.Phoenix, 23 | NewRelic.Telemetry.PhoenixLiveView, 24 | NewRelic.Telemetry.Oban, 25 | NewRelic.Telemetry.Finch, 26 | NewRelic.Telemetry.Absinthe 27 | ] 28 | end 29 | 30 | defp children(enabled: false) do 31 | [] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/new_relic/tracer.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Tracer do 2 | @moduledoc """ 3 | Function Tracing 4 | 5 | To enable function tracing in a particular module, `use NewRelic.Tracer`, 6 | and annotate the functions you want to trace with `@trace`. 7 | 8 | Traced functions will report as: 9 | - Segments in Transaction Traces 10 | - Span Events in Distributed Traces 11 | - Special custom attributes on Transaction Events 12 | 13 | > #### Warning {: .error} 14 | > 15 | > Traced functions will *not* be tail-call-recursive. **Don't use this for recursive functions**. 16 | 17 | #### Example 18 | 19 | Trace a function: 20 | 21 | ```elixir 22 | defmodule MyModule do 23 | use NewRelic.Tracer 24 | 25 | @trace :my_function 26 | def my_function do 27 | # Will report as `MyModule.my_function/0` 28 | end 29 | 30 | @trace :alias 31 | def my_function do 32 | # Will report as `MyModule.my_function:alias/0` 33 | end 34 | end 35 | ``` 36 | 37 | #### Arguments 38 | 39 | By default, arguments are inspected and recorded along with traces. You can opt-out of function argument tracing on individual tracers: 40 | 41 | ```elixir 42 | defmodule SecretModule do 43 | use NewRelic.Tracer 44 | 45 | @trace {:login, args: false} 46 | def login(username, password) do 47 | # do something secret... 48 | end 49 | end 50 | ``` 51 | 52 | This will prevent the argument values from becoming part of Transaction Traces. 53 | 54 | This may also be configured globally via `Application` config. See `NewRelic.Config` for details. 55 | 56 | #### External Service calls 57 | 58 | > #### Finch {: .warning} 59 | > 60 | > `Finch` requests are auto-instrumented, so you don't need to use `category: :external` tracers or call `set_span` if you use `Finch`. 61 | > You may still want to use a normal tracer for functions that make HTTP requests if they do additional work worth instrumenting. 62 | > Automatic `Finch` instrumentation can not inject Distributed Trace headers, so that must still be done manually. 63 | 64 | To manually instrument External Service calls you must give the trace annotation a category. 65 | 66 | You may also call `NewRelic.set_span/2` to provide better naming for metrics & spans, and additionally annotate the outgoing HTTP headers with the Distributed Tracing context to track calls across services. 67 | 68 | ```elixir 69 | defmodule MyExternalService do 70 | use NewRelic.Tracer 71 | 72 | @trace {:request, category: :external} 73 | def request(method, url, headers) do 74 | NewRelic.set_span(:http, url: url, method: method, component: "HttpClient") 75 | headers ++ NewRelic.distributed_trace_headers(:http) 76 | HttpClient.request(method, url, headers) 77 | end 78 | end 79 | ``` 80 | """ 81 | 82 | defmacro __using__(_args) do 83 | quote do 84 | require NewRelic 85 | require NewRelic.Tracer.Macro 86 | require NewRelic.Tracer.Report 87 | Module.register_attribute(__MODULE__, :nr_tracers, accumulate: true) 88 | Module.register_attribute(__MODULE__, :nr_last_tracer, accumulate: false) 89 | @before_compile NewRelic.Tracer.Macro 90 | @on_definition NewRelic.Tracer.Macro 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/new_relic/transaction.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Transaction do 2 | @moduledoc false 3 | 4 | @deprecated "Plug is now auto-instrumented via `telemetry`, please remove manual instrumentation." 5 | defmacro __using__(_) do 6 | quote do 7 | :not_needed! 8 | end 9 | end 10 | 11 | @deprecated "Plug is now auto-instrumented via `telemetry`, please remove manual instrumentation." 12 | def handle_errors(_conn, _error) do 13 | :not_needed! 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/new_relic/transaction/erlang_trace_manager.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Transaction.ErlangTraceManager do 2 | use GenServer 3 | 4 | @moduledoc false 5 | 6 | def start_link(_) do 7 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 8 | end 9 | 10 | def init(:ok) do 11 | {:ok, %{restarts: 0}} 12 | end 13 | 14 | def restart_count() do 15 | GenServer.call(__MODULE__, :restart_count) 16 | end 17 | 18 | def handle_info(:enable, state) do 19 | NewRelic.log(:debug, "ErlangTrace: restart number #{state.restarts + 1}") 20 | enable_erlang_trace() 21 | {:noreply, %{state | restarts: state.restarts + 1}} 22 | end 23 | 24 | def handle_call(:restart_count, _from, state) do 25 | {:reply, state.restarts, state} 26 | end 27 | 28 | def disable_erlang_trace do 29 | NewRelic.Transaction.ErlangTrace.disable() 30 | end 31 | 32 | def enable_erlang_trace do 33 | Supervisor.start_child( 34 | NewRelic.Transaction.ErlangTraceSupervisor, 35 | Supervisor.child_spec(NewRelic.Transaction.ErlangTrace, []) 36 | ) 37 | end 38 | 39 | def enable_erlang_trace(after: after_ms) do 40 | Process.send_after(__MODULE__, :enable, after_ms) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/new_relic/transaction/erlang_trace_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Transaction.ErlangTraceSupervisor do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | def start_link(_) do 7 | Supervisor.start_link(__MODULE__, [], name: __MODULE__) 8 | end 9 | 10 | def init(_) do 11 | enabled? = !Application.get_env(:new_relic_agent, :disable_erlang_trace, false) 12 | 13 | Supervisor.init(children(enabled: enabled?), strategy: :one_for_one) 14 | end 15 | 16 | def children(enabled: true), do: [NewRelic.Transaction.ErlangTrace] 17 | def children(enabled: false), do: [] 18 | end 19 | -------------------------------------------------------------------------------- /lib/new_relic/transaction/event.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Transaction.Event do 2 | defstruct type: "Transaction", 3 | web_duration: nil, 4 | database_duration: nil, 5 | timestamp: nil, 6 | name: nil, 7 | duration: nil, 8 | total_time: nil, 9 | user_attributes: %{} 10 | 11 | @moduledoc false 12 | 13 | def format_events(transactions) do 14 | Enum.map(transactions, &format_event/1) 15 | end 16 | 17 | defp format_event(%__MODULE__{} = transaction) do 18 | [ 19 | %{ 20 | webDuration: transaction.web_duration, 21 | totalTime: transaction.total_time, 22 | databaseDuration: transaction.database_duration, 23 | timestamp: transaction.timestamp, 24 | name: transaction.name, 25 | duration: transaction.duration, 26 | type: transaction.type 27 | }, 28 | NewRelic.Util.Event.process_event(transaction.user_attributes) 29 | ] 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/new_relic/transaction/reporter.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Transaction.Reporter do 2 | alias NewRelic.Transaction 3 | 4 | # This GenServer collects and reports Transaction related data 5 | # - Transaction Events 6 | # - Transaction Metrics 7 | # - Span Events 8 | # - Transaction Errors 9 | # - Transaction Traces 10 | # - Custom Attributes 11 | 12 | @moduledoc false 13 | 14 | def add_attributes(attrs) when is_list(attrs) do 15 | attrs 16 | |> NewRelic.Util.deep_flatten() 17 | |> NewRelic.Util.coerce_attributes() 18 | |> Transaction.Sidecar.add() 19 | end 20 | 21 | def incr_attributes(attrs) do 22 | Transaction.Sidecar.incr(attrs) 23 | end 24 | 25 | def set_transaction_name(custom_name) when is_binary(custom_name) do 26 | Transaction.Sidecar.add(custom_name: custom_name) 27 | end 28 | 29 | def start_transaction(:web, path) do 30 | unless Transaction.Sidecar.tracking?() do 31 | {system_time, start_time_mono} = {System.system_time(), System.monotonic_time()} 32 | 33 | if NewRelic.Util.path_match?(path, NewRelic.Config.ignore_paths()) do 34 | ignore_transaction() 35 | :ignore 36 | else 37 | Transaction.ErlangTrace.trace() 38 | Transaction.Sidecar.track(:web) 39 | Transaction.Sidecar.add(system_time: system_time, start_time_mono: start_time_mono) 40 | :collect 41 | end 42 | end 43 | end 44 | 45 | def start_transaction(:other) do 46 | {system_time, start_time_mono} = {System.system_time(), System.monotonic_time()} 47 | 48 | unless Transaction.Sidecar.tracking?() do 49 | Transaction.ErlangTrace.trace() 50 | Transaction.Sidecar.track(:other) 51 | Transaction.Sidecar.add(system_time: system_time, start_time_mono: start_time_mono) 52 | end 53 | end 54 | 55 | def stop_transaction(:web) do 56 | Transaction.Sidecar.add(end_time_mono: System.monotonic_time()) 57 | Transaction.Sidecar.complete() 58 | end 59 | 60 | def stop_transaction(:other) do 61 | Transaction.Sidecar.add(end_time_mono: System.monotonic_time()) 62 | Transaction.Sidecar.complete() 63 | end 64 | 65 | def ignore_transaction() do 66 | Transaction.Sidecar.ignore() 67 | :ok 68 | end 69 | 70 | def exclude_from_transaction() do 71 | Transaction.Sidecar.exclude() 72 | :ok 73 | end 74 | 75 | def get_transaction() do 76 | %{ 77 | sidecar: Transaction.Sidecar.get_sidecar(), 78 | parent: 79 | case NewRelic.DistributedTrace.read_current_span() do 80 | nil -> self() 81 | {label, ref} -> {self(), label, ref} 82 | end 83 | } 84 | end 85 | 86 | def connect_to_transaction(tx_ref) do 87 | Transaction.Sidecar.connect(tx_ref) 88 | :ok 89 | end 90 | 91 | def disconnect_from_transaction() do 92 | Transaction.Sidecar.disconnect() 93 | :ok 94 | end 95 | 96 | def notice_error(exception, stacktrace) do 97 | if NewRelic.Config.feature?(:error_collector) do 98 | error(%{kind: :error, reason: exception, stack: stacktrace}) 99 | end 100 | 101 | :ok 102 | end 103 | 104 | def error(%{kind: _kind, reason: _reason, stack: _stack} = error) do 105 | Transaction.Sidecar.add(error: true, transaction_error: {:error, error}) 106 | end 107 | 108 | def add_trace_segment(segment) do 109 | Transaction.Sidecar.append(function_segments: segment) 110 | end 111 | 112 | def track_metric(metric) do 113 | Transaction.Sidecar.append(transaction_metrics: metric) 114 | end 115 | 116 | def track_spawn(parent, child, timestamp) do 117 | Transaction.Sidecar.track_spawn(parent, child, timestamp) 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/new_relic/transaction/sidecar_store.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Transaction.SidecarStore do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | def start_link(_) do 7 | Supervisor.start_link(__MODULE__, :ok) 8 | end 9 | 10 | def init(:ok) do 11 | NewRelic.Transaction.Sidecar.setup_stores() 12 | Supervisor.init([], strategy: :one_for_one) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/new_relic/transaction/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Transaction.Supervisor do 2 | use Supervisor 3 | 4 | @moduledoc false 5 | 6 | def start_link(_) do 7 | Supervisor.start_link(__MODULE__, []) 8 | end 9 | 10 | def init(_) do 11 | children = [ 12 | NewRelic.Transaction.ErlangTraceManager, 13 | NewRelic.Transaction.ErlangTraceSupervisor, 14 | NewRelic.Transaction.SidecarStore 15 | ] 16 | 17 | Supervisor.init(children, strategy: :one_for_one) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/new_relic/transaction/trace.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Transaction.Trace do 2 | defstruct start_time: nil, 3 | metric_name: nil, 4 | request_url: nil, 5 | attributes: %{}, 6 | segments: [], 7 | duration: nil, 8 | cat_guid: "", 9 | reserved_for_future_use: nil, 10 | force_persist_flag: false, 11 | xray_session_id: nil, 12 | synthetics_resource_id: "" 13 | 14 | @moduledoc false 15 | 16 | defmodule Segment do 17 | defstruct relative_start_time: nil, 18 | relative_end_time: nil, 19 | metric_name: nil, 20 | attributes: %{}, 21 | children: [], 22 | class_name: nil, 23 | method_name: nil, 24 | parent_id: nil, 25 | id: nil, 26 | pid: nil 27 | 28 | @moduledoc false 29 | end 30 | 31 | @unused_map %{} 32 | 33 | def format_traces(traces) do 34 | Enum.map(traces, &format_trace/1) 35 | end 36 | 37 | defp format_trace(%__MODULE__{} = trace) do 38 | trace_segments = format_segments(trace) 39 | trace_details = [trace.start_time, @unused_map, @unused_map, trace_segments, trace.attributes] 40 | 41 | [ 42 | trace.start_time, 43 | trace.duration, 44 | trace.metric_name, 45 | trace.request_url, 46 | trace_details, 47 | trace.cat_guid, 48 | trace.reserved_for_future_use, 49 | trace.force_persist_flag, 50 | trace.xray_session_id, 51 | trace.synthetics_resource_id 52 | ] 53 | end 54 | 55 | defp format_segments(%{ 56 | segments: [first_segment | _] = segments, 57 | duration: duration, 58 | metric_name: metric_name 59 | }) do 60 | attributes = first_segment.attributes |> NewRelic.Util.coerce_attributes() 61 | 62 | [ 63 | 0, 64 | duration, 65 | metric_name, 66 | attributes, 67 | Enum.map(segments, &format_child_segments/1) 68 | ] 69 | end 70 | 71 | defp format_child_segments(%Segment{} = segment) do 72 | [ 73 | segment.relative_start_time, 74 | segment.relative_end_time, 75 | segment.metric_name, 76 | segment.attributes |> NewRelic.Util.coerce_attributes(), 77 | Enum.map(segment.children, &format_child_segments/1), 78 | segment.class_name, 79 | segment.method_name 80 | ] 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/new_relic/util/apdex.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Util.Apdex do 2 | @moduledoc false 3 | 4 | # https://en.wikipedia.org/wiki/Apdex 5 | 6 | def calculate(dur, apdex_t) when dur < apdex_t, do: :satisfying 7 | def calculate(dur, apdex_t) when dur < apdex_t * 4, do: :tolerating 8 | def calculate(_dur, _apdex_t), do: :frustrating 9 | 10 | def label(:satisfying), do: "S" 11 | def label(:tolerating), do: "T" 12 | def label(:frustrating), do: "F" 13 | def label(:ignore), do: nil 14 | end 15 | -------------------------------------------------------------------------------- /lib/new_relic/util/conditional_compile.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Util.ConditionalCompile do 2 | @moduledoc false 3 | def match?(version) do 4 | Version.match?(System.version(), version) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/new_relic/util/error.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Util.Error do 2 | # Helper functions for normalizing and formatting errors 3 | 4 | @moduledoc false 5 | 6 | def normalize(kind, exception, stacktrace, initial_call \\ nil) 7 | 8 | def normalize(kind, exception, stacktrace, initial_call) do 9 | normalized_error = Exception.normalize(kind, exception, stacktrace) 10 | 11 | exception_type = format_type(kind, normalized_error) 12 | exception_reason = format_reason(kind, normalized_error) 13 | exception_stacktrace = format_stacktrace(stacktrace, initial_call) 14 | 15 | {exception_type, exception_reason, exception_stacktrace} 16 | end 17 | 18 | defp format_type(:error, %ErlangError{original: {_reason, {module, function, args}}}), 19 | do: Exception.format_mfa(module, function, length(args)) 20 | 21 | defp format_type(_, %{__exception__: true, __struct__: struct}), do: inspect(struct) 22 | defp format_type(:exit, _reason), do: "EXIT" 23 | 24 | def format_reason(:error, %ErlangError{original: {reason, {module, function, args}}}), 25 | do: "(" <> Exception.format_mfa(module, function, length(args)) <> ") " <> inspect(reason) 26 | 27 | def format_reason(:error, error), 28 | do: 29 | :error 30 | |> Exception.format_banner(error) 31 | |> String.replace("** ", "") 32 | 33 | def format_reason(:exit, {reason, {module, function, args}}), 34 | do: "(" <> Exception.format_mfa(module, function, length(args)) <> ") " <> inspect(reason) 35 | 36 | def format_reason(:exit, %{__exception__: true} = error), do: format_reason(:error, error) 37 | def format_reason(:exit, reason), do: inspect(reason) 38 | 39 | def format_stacktrace(stacktrace, initial_call), 40 | do: 41 | maybe_remove_args_from_stacktrace(stacktrace) 42 | |> List.wrap() 43 | |> prepend_initial_call(initial_call) 44 | |> Enum.map(fn 45 | line when is_binary(line) -> line 46 | entry when is_tuple(entry) -> Exception.format_stacktrace_entry(entry) 47 | end) 48 | 49 | defp prepend_initial_call(stacktrace, {mod, fun, args}) do 50 | if NewRelic.Config.feature?(:stacktrace_argument_collection) do 51 | stacktrace ++ [{mod, fun, args, []}] 52 | else 53 | stacktrace ++ [{mod, fun, ["DISABLED (arity: #{length(args)})"], []}] 54 | end 55 | end 56 | 57 | defp prepend_initial_call(stacktrace, _) do 58 | stacktrace 59 | end 60 | 61 | defp maybe_remove_args_from_stacktrace(stacktrace) do 62 | if NewRelic.Config.feature?(:stacktrace_argument_collection) do 63 | stacktrace 64 | else 65 | remove_args_from_stacktrace(stacktrace) 66 | end 67 | end 68 | 69 | defp remove_args_from_stacktrace([{mod, fun, [_ | _] = args, info} | rest]), 70 | do: [{mod, fun, ["DISABLED (arity: #{length(args)})"], info} | rest] 71 | 72 | defp remove_args_from_stacktrace(stacktrace) when is_list(stacktrace), 73 | do: stacktrace 74 | end 75 | -------------------------------------------------------------------------------- /lib/new_relic/util/event.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Util.Event do 2 | @moduledoc false 3 | 4 | def process_event(event), do: Map.new(event, &process_attr/1) 5 | 6 | defp process_attr({key, val}) when is_binary(val), do: {key, val |> limit_size} 7 | defp process_attr({key, val}) when is_bitstring(val), do: {key, val |> inspect |> limit_size} 8 | defp process_attr({key, val}) when is_pid(val), do: {key, val |> inspect} 9 | defp process_attr({key, val}), do: {key, val} 10 | 11 | @max_string 4096 12 | defp limit_size(string) when byte_size(string) < @max_string, do: string 13 | 14 | defp limit_size(string) do 15 | index = find_truncation_point(string) 16 | String.slice(string, 0, index) 17 | end 18 | 19 | defp find_truncation_point(string, len \\ 0) 20 | 21 | defp find_truncation_point("", len), do: len 22 | 23 | defp find_truncation_point(string, len) do 24 | case next_grapheme_size(string) do 25 | {char_size, rest} when len + char_size < @max_string -> 26 | find_truncation_point(rest, len + char_size) 27 | 28 | _ -> 29 | len 30 | end 31 | end 32 | 33 | defp next_grapheme_size(string) do 34 | {grapheme, rest} = String.next_grapheme(string) 35 | {byte_size(grapheme), rest} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/new_relic/util/http.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Util.HTTP do 2 | @moduledoc false 3 | 4 | @gzip {~c"content-encoding", ~c"gzip"} 5 | 6 | def post(url, body, headers) when is_binary(body) do 7 | headers = [@gzip | Enum.map(headers, fn {k, v} -> {~c"#{k}", ~c"#{v}"} end)] 8 | request = {~c"#{url}", headers, ~c"application/json", :zlib.gzip(body)} 9 | 10 | with {:ok, {{_, status_code, _}, _headers, body}} <- 11 | :httpc.request(:post, request, http_options(), []) do 12 | {:ok, %{status_code: status_code, body: to_string(body)}} 13 | end 14 | end 15 | 16 | def post(url, body, headers) do 17 | body = NewRelic.JSON.encode!(body) 18 | post(url, body, headers) 19 | rescue 20 | error -> 21 | NewRelic.log(:debug, "Unable to JSON encode: #{inspect(body)}") 22 | {:error, Exception.message(error)} 23 | end 24 | 25 | def get(url, headers \\ [], opts \\ []) do 26 | headers = Enum.map(headers, fn {k, v} -> {~c"#{k}", ~c"#{v}"} end) 27 | request = {~c"#{url}", headers} 28 | 29 | with {:ok, {{_, status_code, _}, _, body}} <- 30 | :httpc.request(:get, request, http_options(opts), []) do 31 | {:ok, %{status_code: status_code, body: to_string(body)}} 32 | end 33 | end 34 | 35 | # Certs are from `CAStore`. 36 | # https://github.com/elixir-mint/castore 37 | 38 | # SSL configured according to EEF Security guide: 39 | # https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl 40 | defp http_options(opts \\ []) do 41 | env_opts = Application.get_env(:new_relic_agent, :httpc_request_options, []) 42 | 43 | [ 44 | connect_timeout: 1000, 45 | ssl: [ 46 | verify: :verify_peer, 47 | cacertfile: CAStore.file_path(), 48 | customize_hostname_check: [ 49 | match_fun: :public_key.pkix_verify_hostname_match_fun(:https) 50 | ] 51 | ] 52 | ] 53 | |> Keyword.merge(opts) 54 | |> Keyword.merge(env_opts) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/new_relic/util/priority_queue.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Util.PriorityQueue do 2 | # This is a simple PriorityQueue based on erlang's gb_trees used to 3 | # keep the highest priority events when we reach max harvest size 4 | 5 | @moduledoc false 6 | 7 | def new() do 8 | :gb_trees.empty() 9 | end 10 | 11 | def insert(tree, max_size, key, value) do 12 | insert(tree, :gb_trees.size(tree), max_size, key, value) 13 | end 14 | 15 | def insert(tree, size, max_size, key, value) when size >= max_size do 16 | {_k, _v, tree} = 17 | {key, differentiator()} 18 | |> :gb_trees.insert(value, tree) 19 | |> :gb_trees.take_smallest() 20 | 21 | tree 22 | end 23 | 24 | def insert(tree, _size, _max_size, key, value) do 25 | {key, differentiator()} 26 | |> :gb_trees.insert(value, tree) 27 | end 28 | 29 | def values(tree) do 30 | :gb_trees.values(tree) 31 | end 32 | 33 | defp differentiator() do 34 | :erlang.unique_integer() 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/new_relic/util/request_start.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Util.RequestStart do 2 | @moduledoc false 3 | 4 | def parse("t=" <> time), do: parse(time) 5 | 6 | def parse(time) do 7 | with {time, _} <- Float.parse(time), 8 | :next <- check_time_unit(time / 1000_000), 9 | :next <- check_time_unit(time / 1000), 10 | :next <- check_time_unit(time) do 11 | :error 12 | else 13 | {:ok, queue_start_s} -> {:ok, queue_start_s} 14 | _ -> :error 15 | end 16 | end 17 | 18 | @earliest ~N[2000-01-01 00:00:00] 19 | |> DateTime.from_naive!("Etc/UTC") 20 | |> DateTime.to_unix() 21 | 22 | defp check_time_unit(time) do 23 | (time > @earliest && {:ok, time}) || :next 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/new_relic/util/vendor.ex: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Util.Vendor do 2 | @moduledoc false 3 | 4 | def maybe_add_vendors(util, options \\ []) do 5 | %{} 6 | |> maybe_add_aws(options) 7 | |> maybe_add_kubernetes(options) 8 | |> maybe_add_docker(options) 9 | |> case do 10 | vendors when map_size(vendors) == 0 -> util 11 | vendors -> Map.put(util, :vendors, vendors) 12 | end 13 | end 14 | 15 | @aws_url "http://169.254.169.254/2016-09-02/dynamic/instance-identity/document" 16 | defp maybe_add_aws(vendors, options) do 17 | Keyword.get(options, :aws_url, @aws_url) 18 | |> aws_vendor_map() 19 | |> case do 20 | nil -> vendors 21 | aws_hash -> Map.put(vendors, :aws, aws_hash) 22 | end 23 | end 24 | 25 | defp maybe_add_kubernetes(vendors, _options) do 26 | System.get_env("KUBERNETES_SERVICE_HOST") 27 | |> case do 28 | nil -> vendors 29 | value -> Map.put(vendors, :kubernetes, %{kubernetes_service_host: value}) 30 | end 31 | end 32 | 33 | @cgroup_filename "/proc/self/cgroup" 34 | defp maybe_add_docker(vendors, options) do 35 | Keyword.get(options, :cgroup_filename, @cgroup_filename) 36 | |> docker_vendor_map() 37 | |> case do 38 | nil -> vendors 39 | docker -> Map.put(vendors, :docker, docker) 40 | end 41 | end 42 | 43 | @cgroup_matcher ~r/\d+:.*cpu[,:].*(?[0-9a-f]{64}).*/ 44 | defp docker_vendor_map(cgroup_filename) do 45 | File.read(cgroup_filename) 46 | |> case do 47 | {:ok, cgroup_file} -> 48 | cgroup_file 49 | |> String.split("\n", trim: true) 50 | |> Enum.find_value(&Regex.named_captures(@cgroup_matcher, &1)) 51 | 52 | _ -> 53 | nil 54 | end 55 | end 56 | 57 | @aws_vendor_data ["availabilityZone", "instanceId", "instanceType"] 58 | defp aws_vendor_map(url) do 59 | case :httpc.request(:get, {~c(#{url}), []}, [{:timeout, 100}], []) do 60 | {:ok, {{_, 200, ~c"OK"}, _headers, body}} -> 61 | case NewRelic.JSON.decode(to_string(body)) do 62 | {:ok, data} -> Map.take(data, @aws_vendor_data) 63 | _ -> nil 64 | end 65 | 66 | _error -> 67 | nil 68 | end 69 | rescue 70 | exception -> 71 | NewRelic.log(:error, "Failed to fetch AWS metadata. #{inspect(exception)}") 72 | nil 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/newrelic/elixir_agent" 5 | 6 | def project do 7 | [ 8 | app: :new_relic_agent, 9 | description: "New Relic's Open-Source Elixir Agent", 10 | version: agent_version(), 11 | elixir: "~> 1.11", 12 | build_embedded: Mix.env() == :prod, 13 | start_permanent: Mix.env() == :prod, 14 | name: "New Relic Elixir Agent", 15 | source_url: @source_url, 16 | elixirc_paths: elixirc_paths(Mix.env()), 17 | package: package(), 18 | deps: deps(), 19 | docs: docs() 20 | ] 21 | end 22 | 23 | def application do 24 | [ 25 | extra_applications: [:logger, :inets, :ssl, {:os_mon, :optional}], 26 | mod: {NewRelic.Application, []} 27 | ] 28 | end 29 | 30 | defp package do 31 | [ 32 | files: ["lib", "mix.exs", "README.md", "CHANGELOG.md", "VERSION"], 33 | maintainers: ["Vince Foley"], 34 | licenses: ["Apache-2.0"], 35 | links: %{ 36 | "Changelog" => "#{@source_url}/blob/master/CHANGELOG.md", 37 | "GitHub" => @source_url 38 | } 39 | ] 40 | end 41 | 42 | defp deps do 43 | [ 44 | {:ex_doc, ">= 0.0.0", only: :dev}, 45 | {:castore, ">= 0.1.0"}, 46 | {:jason, "~> 1.0", optional: true}, 47 | {:telemetry, "~> 0.4 or ~> 1.0"}, 48 | # Instrumentation: 49 | {:plug, ">= 1.10.4", optional: true}, 50 | {:plug_cowboy, ">= 2.4.0", optional: true}, 51 | {:bandit, ">= 1.0.0", optional: true}, 52 | {:phoenix, ">= 1.5.5", optional: true}, 53 | {:ecto_sql, ">= 3.4.0", optional: true}, 54 | {:ecto, ">= 3.9.5", optional: true}, 55 | {:redix, ">= 0.11.0", optional: true}, 56 | {:oban, ">= 2.0.0", optional: true}, 57 | {:finch, ">= 0.18.0", optional: true}, 58 | {:absinthe, ">= 1.6.0", optional: true} 59 | ] 60 | end 61 | 62 | defp docs do 63 | [ 64 | main: "readme", 65 | source_ref: "v" <> agent_version(), 66 | extras: ["README.md", "CHANGELOG.md"] 67 | ] 68 | end 69 | 70 | defp elixirc_paths(:test), do: ["lib", "test/support"] 71 | defp elixirc_paths(_), do: ["lib"] 72 | 73 | @agent_version File.read!("VERSION") |> String.trim() 74 | def agent_version, do: @agent_version 75 | end 76 | -------------------------------------------------------------------------------- /test/aggregate_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AggregateTest do 2 | use ExUnit.Case 3 | 4 | alias NewRelic.Aggregate 5 | alias NewRelic.Harvest.Collector 6 | 7 | test "Aggregate metrics" do 8 | metric = %Aggregate{ 9 | meta: %{key: "value", foo: "bar"}, 10 | values: %{duration: 5, call_count: 2, foo: 1} 11 | } 12 | 13 | values = %{duration: 3, call_count: 1, bar: 1} 14 | 15 | result = Aggregate.merge(metric, values) 16 | assert result.meta == %{key: "value", foo: "bar"} 17 | assert result.values == %{duration: 8, call_count: 3, foo: 1, bar: 1} 18 | end 19 | 20 | test "Annotate metrics w/ averages" do 21 | metric = %Aggregate{meta: %{call_count: true}, values: %{duration: 10, call_count: 2, foo: 1}} 22 | 23 | annotated = Aggregate.annotate(metric) 24 | assert annotated.duration == 10 25 | assert annotated.avg_duration == 5 26 | end 27 | 28 | test "unless there's no call count" do 29 | metric = %Aggregate{meta: %{call_count: false}, values: %{duration: 10, foo: 1}} 30 | 31 | annotated = Aggregate.annotate(metric) 32 | assert annotated[:avg_duration] == nil 33 | end 34 | 35 | test "Aggregate.Reporter collects aggregated metrics" do 36 | TestHelper.restart_harvest_cycle(Collector.CustomEvent.HarvestCycle) 37 | 38 | NewRelic.report_aggregate(%{meta: "data"}, %{duration: 5}) 39 | NewRelic.report_aggregate(%{meta: "data"}, %{duration: 5}) 40 | NewRelic.report_aggregate(%{meta: "data"}, %{duration: 5}) 41 | 42 | TestHelper.trigger_report(NewRelic.Aggregate.Reporter) 43 | 44 | events = TestHelper.gather_harvest(Collector.CustomEvent.Harvester) 45 | 46 | assert TestHelper.find_event(events, %{category: "Metric", meta: "data", duration: 15}) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/collector_protocol_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CollectorProtocolTest do 2 | use ExUnit.Case 3 | alias NewRelic.Harvest.Collector 4 | 5 | test "Connect payload" do 6 | [payload] = Collector.Connect.payload() 7 | 8 | assert get_in(payload, [:utilization, :total_ram_mib]) 9 | |> is_integer 10 | 11 | assert get_in(payload, [:metadata]) 12 | |> is_map 13 | 14 | assert get_in(payload, [:environment]) 15 | |> Enum.find(&match?(["OTP Version", _], &1)) 16 | 17 | assert get_in(payload, [:environment]) 18 | |> Enum.find(&match?(["ERTS Version", _], &1)) 19 | 20 | assert Map.has_key?(payload, :display_host) 21 | 22 | NewRelic.JSON.encode!(payload) 23 | end 24 | 25 | test "determine correct collector host" do 26 | assert "collector.newrelic.com" = Collector.Protocol.determine_host(nil, nil) 27 | assert "collector.eu01.nr-data.net" = Collector.Protocol.determine_host(nil, "eu01") 28 | assert "cool.newrelic.com" = Collector.Protocol.determine_host("cool.newrelic.com", nil) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ConfigTest do 2 | use ExUnit.Case 3 | 4 | test "Version read from file" do 5 | assert NewRelic.Config.agent_version() == Mix.Project.config()[:version] 6 | end 7 | 8 | test "Logger output device" do 9 | inital_logger = NewRelic.Logger.initial_logger() 10 | TestHelper.run_with(:nr_config, log: "stdout") 11 | 12 | assert :stdio == NewRelic.Logger.initial_logger() 13 | 14 | TestHelper.run_with(:nr_config, log: "some_file.log") 15 | assert {:file, "some_file.log"} == NewRelic.Logger.initial_logger() 16 | 17 | TestHelper.run_with(:nr_config, log: "Logger") 18 | assert :logger == NewRelic.Logger.initial_logger() 19 | 20 | assert inital_logger == NewRelic.Logger.initial_logger() 21 | end 22 | 23 | test "hydrate automatic attributes" do 24 | System.put_env("ENV_VAR_NAME", "env-var-value") 25 | 26 | TestHelper.run_with(:application_config, 27 | automatic_attributes: [ 28 | env_var: {:system, "ENV_VAR_NAME"}, 29 | function_call: {String, :upcase, ["fun"]}, 30 | raw: "attribute" 31 | ] 32 | ) 33 | 34 | assert NewRelic.Init.determine_automatic_attributes() == %{ 35 | env_var: "env-var-value", 36 | raw: "attribute", 37 | function_call: "FUN" 38 | } 39 | end 40 | 41 | test "Can configure error collecting via ENV and Application" do 42 | on_exit(fn -> 43 | System.delete_env("NEW_RELIC_ERROR_COLLECTOR_ENABLED") 44 | Application.delete_env(:new_relic_agent, :error_collector_enabled) 45 | NewRelic.Init.init_features() 46 | end) 47 | 48 | # Via ENV 49 | System.put_env("NEW_RELIC_ERROR_COLLECTOR_ENABLED", "false") 50 | NewRelic.Init.init_features() 51 | refute NewRelic.Config.feature?(:error_collector) 52 | 53 | # Via Application 54 | System.delete_env("NEW_RELIC_ERROR_COLLECTOR_ENABLED") 55 | Application.put_env(:new_relic_agent, :error_collector_enabled, true) 56 | NewRelic.Init.init_features() 57 | assert NewRelic.Config.feature?(:error_collector) 58 | 59 | # ENV over Application 60 | System.put_env("NEW_RELIC_ERROR_COLLECTOR_ENABLED", "true") 61 | Application.put_env(:new_relic_agent, :error_collector_enabled, false) 62 | NewRelic.Init.init_features() 63 | assert NewRelic.Config.feature?(:error_collector) 64 | 65 | # Default 66 | System.delete_env("NEW_RELIC_ERROR_COLLECTOR_ENABLED") 67 | Application.delete_env(:new_relic_agent, :error_collector_enabled) 68 | NewRelic.Init.init_features() 69 | assert NewRelic.Config.feature?(:error_collector) 70 | end 71 | 72 | test "Parse multiple app names" do 73 | assert "Two" in NewRelic.Init.parse_app_names("One; Two") 74 | assert length(NewRelic.Init.parse_app_names("One; Two")) == 2 75 | 76 | assert length(NewRelic.Init.parse_app_names("One Name")) == 1 77 | end 78 | 79 | test "Parse labels" do 80 | labels = NewRelic.Init.parse_labels("key1:value1;key2:value2; key3 :value3;stray ") 81 | 82 | assert ["key3", "value3"] in labels 83 | assert length(labels) == 3 84 | 85 | assert [] == NewRelic.Init.parse_labels(nil) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/dimensional_metric_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DimensionalMetricTest do 2 | use ExUnit.Case 3 | 4 | test "reports dimensional metrics" do 5 | TestHelper.restart_harvest_cycle(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle) 6 | 7 | NewRelic.report_dimensional_metric(:count, "memory.foo_baz", 100, %{cpu: 1000}) 8 | NewRelic.report_dimensional_metric(:summary, "memory.foo_bar", 50, %{cpu: 2000}) 9 | 10 | [%{common: common, metrics: metrics_map}] = 11 | TestHelper.gather_harvest(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.Harvester) 12 | 13 | metrics = Map.values(metrics_map) 14 | assert common["interval.ms"] > 0 15 | assert common["timestamp"] > 0 16 | 17 | assert length(metrics) == 2 18 | [metric1, metric2] = metrics 19 | assert metric1.name == "memory.foo_baz" 20 | assert metric1.type == :count 21 | 22 | assert metric2.name == "memory.foo_bar" 23 | assert metric2.type == :summary 24 | end 25 | 26 | test "gauge dimensional metric is updated" do 27 | TestHelper.restart_harvest_cycle(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle) 28 | 29 | NewRelic.report_dimensional_metric(:gauge, "mem_percent.foo_baz", 10, %{cpu: 1000}) 30 | NewRelic.report_dimensional_metric(:gauge, "mem_percent.foo_baz", 40, %{cpu: 1000}) 31 | NewRelic.report_dimensional_metric(:gauge, "mem_percent.foo_baz", 90, %{cpu: 1000}) 32 | 33 | [%{metrics: metrics_map}] = 34 | TestHelper.gather_harvest(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.Harvester) 35 | 36 | metrics = Map.values(metrics_map) 37 | 38 | assert length(metrics) == 1 39 | [metric] = metrics 40 | assert metric.name == "mem_percent.foo_baz" 41 | assert metric.type == :gauge 42 | assert metric.value == 90 43 | end 44 | 45 | test "count dimensional metric is updated" do 46 | TestHelper.restart_harvest_cycle(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle) 47 | 48 | NewRelic.report_dimensional_metric(:count, "OOM", 1, %{cpu: 1000}) 49 | NewRelic.report_dimensional_metric(:count, "OOM", 1, %{cpu: 1000}) 50 | NewRelic.report_dimensional_metric(:count, "OOM", 2, %{cpu: 1000}) 51 | 52 | [%{metrics: metrics_map}] = 53 | TestHelper.gather_harvest(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.Harvester) 54 | 55 | metrics = Map.values(metrics_map) 56 | 57 | assert length(metrics) == 1 58 | [metric] = metrics 59 | assert metric.name == "OOM" 60 | assert metric.type == :count 61 | assert metric.value == 4 62 | end 63 | 64 | test "summary dimensional metric is updated" do 65 | TestHelper.restart_harvest_cycle(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.HarvestCycle) 66 | 67 | NewRelic.report_dimensional_metric(:summary, "duration", 40.5, %{cpu: 1000}) 68 | NewRelic.report_dimensional_metric(:summary, "duration", 20.5, %{cpu: 1000}) 69 | NewRelic.report_dimensional_metric(:summary, "duration", 9.5, %{cpu: 1000}) 70 | NewRelic.report_dimensional_metric(:summary, "duration", 55.5, %{cpu: 1000}) 71 | 72 | [%{metrics: metrics_map}] = 73 | TestHelper.gather_harvest(NewRelic.Harvest.TelemetrySdk.DimensionalMetrics.Harvester) 74 | 75 | metrics = Map.values(metrics_map) 76 | 77 | assert length(metrics) == 1 78 | [metric] = metrics 79 | assert metric.name == "duration" 80 | assert metric.type == :summary 81 | assert metric.value.sum == 126 82 | assert metric.value.min == 9.5 83 | assert metric.value.max == 55.5 84 | assert metric.value.count == 4 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/erlang_trace_overload_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ErlangTraceOverloadTest do 2 | use ExUnit.Case 3 | 4 | @test_queue_len 1 5 | @test_backoff 100 6 | 7 | @tag :capture_log 8 | test "Handle process spawn overload in ErlangTrace" do 9 | TestHelper.run_with(:application_config, overload_queue_len: @test_queue_len) 10 | TestHelper.run_with(:application_config, overload_backoff: @test_backoff) 11 | 12 | NewRelic.disable_erlang_trace() 13 | NewRelic.enable_erlang_trace() 14 | 15 | on_exit(fn -> 16 | NewRelic.disable_erlang_trace() 17 | NewRelic.enable_erlang_trace() 18 | end) 19 | 20 | first_pid = Process.whereis(NewRelic.Transaction.ErlangTrace) 21 | Process.monitor(first_pid) 22 | 23 | Task.async(fn -> 24 | NewRelic.start_transaction("Overload", "test") 25 | 26 | 1..200 27 | |> Enum.to_list() 28 | |> Enum.map(fn _ -> 29 | Task.async(fn -> 30 | Process.sleep(1_000) 31 | end) 32 | end) 33 | |> Enum.map(fn task -> 34 | Task.await(task, :infinity) 35 | end) 36 | 37 | NewRelic.stop_transaction() 38 | end) 39 | |> Task.await(:infinity) 40 | 41 | # ErlangTrace will give up when it's overloaded all existing tracers will go away 42 | assert_receive {:DOWN, _ref, _, ^first_pid, {:shutdown, :overload}}, 1_000 43 | 44 | # ErlangTrace will be restarted after a backoff 45 | Process.sleep(@test_backoff * 2) 46 | 47 | second_pid = NewRelic.Transaction.ErlangTrace |> Process.whereis() 48 | assert is_pid(second_pid) 49 | 50 | assert first_pid != second_pid 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/erlang_trace_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ErlangTraceTest do 2 | use ExUnit.Case, async: false 3 | 4 | test "disable and re-enable Agent's usage of `:erlang.trace`" do 5 | first_pid = Process.whereis(NewRelic.Transaction.ErlangTrace) 6 | assert is_pid(first_pid) 7 | Process.monitor(first_pid) 8 | 9 | NewRelic.disable_erlang_trace() 10 | 11 | on_exit(fn -> 12 | NewRelic.enable_erlang_trace() 13 | end) 14 | 15 | assert_receive {:DOWN, _ref, _, ^first_pid, _} 16 | 17 | NewRelic.enable_erlang_trace() 18 | 19 | second_pid = Process.whereis(NewRelic.Transaction.ErlangTrace) 20 | assert is_pid(second_pid) 21 | assert first_pid != second_pid 22 | end 23 | 24 | test "config option to disable at boot" do 25 | restart_erlang_trace_supervisor() 26 | on_exit(fn -> restart_erlang_trace_supervisor() end) 27 | 28 | # Make sure it starts up with the default setting 29 | assert Process.whereis(NewRelic.Transaction.ErlangTraceSupervisor) 30 | assert Process.whereis(NewRelic.Transaction.ErlangTrace) 31 | 32 | # Pretend the app is starting up with the config option 33 | TestHelper.run_with(:application_config, disable_erlang_trace: true) 34 | restart_erlang_trace_supervisor() 35 | 36 | # Make sure we didn't start the ErlangTrace process 37 | assert Process.whereis(NewRelic.Transaction.ErlangTraceSupervisor) 38 | refute Process.whereis(NewRelic.Transaction.ErlangTrace) 39 | end 40 | 41 | defp restart_erlang_trace_supervisor() do 42 | supervisor = Process.whereis(NewRelic.Transaction.ErlangTraceSupervisor) 43 | Process.monitor(supervisor) 44 | Process.exit(supervisor, :kill) 45 | assert_receive {:DOWN, _ref, _, ^supervisor, _} 46 | 47 | Process.sleep(100) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/init_test.exs: -------------------------------------------------------------------------------- 1 | defmodule InitTest do 2 | use ExUnit.Case 3 | 4 | test "check for region prefix in license_key" do 5 | refute NewRelic.Init.determine_region("08a2ad66c637a29c3982469a3fe9999999999999") 6 | 7 | assert "eu01" == NewRelic.Init.determine_region("eu01xx66c637a29c3982469a3fe9999999999999") 8 | assert "gov01" == NewRelic.Init.determine_region("gov01x66c637a29c3982469a3fe9999999999999") 9 | assert "foo1234" == NewRelic.Init.determine_region("foo1234xc637a29c3982469a3fe9999999999999") 10 | assert "20foo" == NewRelic.Init.determine_region("20foox66c637a29c3982469a3fe9999999999999") 11 | assert "eu01" == NewRelic.Init.determine_region("eu01xeu02x37a29c3982469a3fe9999999999999") 12 | end 13 | 14 | test "handle config default properly" do 15 | on_exit(fn -> 16 | Application.delete_env(:new_relic_agent, :harvest_enabled) 17 | System.delete_env("NEW_RELIC_HARVEST_ENABLED") 18 | NewRelic.Init.init_config() 19 | end) 20 | 21 | Application.put_env(:new_relic_agent, :harvest_enabled, true) 22 | NewRelic.Init.init_config() 23 | assert NewRelic.Config.get(:harvest_enabled) 24 | 25 | Application.put_env(:new_relic_agent, :harvest_enabled, false) 26 | NewRelic.Init.init_config() 27 | refute NewRelic.Config.get(:harvest_enabled) 28 | 29 | System.put_env("NEW_RELIC_HARVEST_ENABLED", "true") 30 | NewRelic.Init.init_config() 31 | assert NewRelic.Config.get(:harvest_enabled) 32 | 33 | System.put_env("NEW_RELIC_HARVEST_ENABLED", "false") 34 | NewRelic.Init.init_config() 35 | refute NewRelic.Config.get(:harvest_enabled) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/logger_test.exs: -------------------------------------------------------------------------------- 1 | defmodule LoggerTest do 2 | use ExUnit.Case 3 | 4 | test "memory Logger" do 5 | previous_logger = GenServer.call(NewRelic.Logger, {:logger, :memory}) 6 | 7 | try do 8 | NewRelic.log(:warning, "OH_NO!") 9 | 10 | log = GenServer.call(NewRelic.Logger, :flush) 11 | assert log =~ "[WARN]" 12 | assert log =~ "OH_NO" 13 | after 14 | GenServer.call(NewRelic.Logger, {:replace, previous_logger}) 15 | end 16 | end 17 | 18 | test "file Logger" do 19 | previous_logger = GenServer.call(NewRelic.Logger, {:logger, {:file, "tmp/test.log"}}) 20 | 21 | try do 22 | NewRelic.log(:error, "OH_NO!") 23 | 24 | :timer.sleep(100) 25 | log = File.read!("tmp/test.log") 26 | assert log =~ "[ERROR]" 27 | assert log =~ "OH_NO" 28 | after 29 | File.rm!("tmp/test.log") 30 | GenServer.call(NewRelic.Logger, {:replace, previous_logger}) 31 | end 32 | end 33 | 34 | @tag :capture_log 35 | test "Logger logger" do 36 | previous_logger = GenServer.call(NewRelic.Logger, {:logger, :logger}) 37 | NewRelic.log(:info, "HELLO") 38 | NewRelic.log(:error, "DANG") 39 | NewRelic.log(:warning, "OOPS") 40 | NewRelic.log(:debug, "SHHH") 41 | GenServer.call(NewRelic.Logger, {:replace, previous_logger}) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/metric_error_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MetricErrorTest do 2 | use ExUnit.Case 3 | alias NewRelic.Harvest.Collector 4 | 5 | defmodule CustomError do 6 | defexception [:message, :expected] 7 | end 8 | 9 | @tag :capture_log 10 | test "Catch and record error Metric for unexpected errors" do 11 | TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle) 12 | start_supervised({Task.Supervisor, name: TestSupervisor}) 13 | 14 | {:exit, {_exception, _stacktrace}} = 15 | Task.Supervisor.async_nolink(TestSupervisor, fn -> 16 | raise "BAD_TIMES" 17 | end) 18 | |> Task.yield() 19 | 20 | {:exit, {_exception, _stacktrace}} = 21 | Task.Supervisor.async_nolink(TestSupervisor, fn -> 22 | raise "BAD_TIMES" 23 | end) 24 | |> Task.yield() 25 | 26 | {:exit, {_exception, _stacktrace}} = 27 | Task.Supervisor.async_nolink(TestSupervisor, fn -> 28 | raise CustomError, message: "BAD_TIMES", expected: true 29 | end) 30 | |> Task.yield() 31 | 32 | metrics = TestHelper.gather_harvest(Collector.Metric.Harvester) 33 | 34 | assert TestHelper.find_metric(metrics, "Errors/all", 2) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/metric_harvester_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MetricHarvesterTest do 2 | use ExUnit.Case 3 | 4 | alias NewRelic.Harvest 5 | alias NewRelic.Harvest.Collector 6 | 7 | test "Harvester - collect and aggregate some metrics" do 8 | {:ok, harvester} = 9 | DynamicSupervisor.start_child( 10 | Collector.Metric.HarvesterSupervisor, 11 | Collector.Metric.Harvester 12 | ) 13 | 14 | metric1 = %NewRelic.Metric{name: "TestMetric", call_count: 1, total_call_time: 100} 15 | metric2 = %NewRelic.Metric{name: "TestMetric", call_count: 1, total_call_time: 50} 16 | GenServer.cast(harvester, {:report, metric1}) 17 | GenServer.cast(harvester, {:report, metric2}) 18 | 19 | # Verify that the metric is encoded as the collector desires 20 | metrics = GenServer.call(harvester, :gather_harvest) 21 | [metric] = metrics 22 | [metric_ident, metric_values] = metric 23 | assert metric_ident == %{name: "TestMetric", scope: ""} 24 | assert metric_values == [2, 150, 0, 0, 0, 0] 25 | 26 | # Verify that the Harvester shuts down w/o error 27 | Process.monitor(harvester) 28 | Harvest.HarvestCycle.send_harvest(Collector.Metric.HarvesterSupervisor, harvester) 29 | assert_receive {:DOWN, _ref, _, ^harvester, :shutdown}, 1000 30 | end 31 | 32 | test "harvest cycle" do 33 | TestHelper.run_with(:application_config, data_report_period: 300) 34 | TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle) 35 | 36 | first = Harvest.HarvestCycle.current_harvester(Collector.Metric.HarvestCycle) 37 | Process.monitor(first) 38 | 39 | # Wait until harvest swap 40 | assert_receive {:DOWN, _ref, _, ^first, :shutdown}, 1000 41 | 42 | second = Harvest.HarvestCycle.current_harvester(Collector.Metric.HarvestCycle) 43 | Process.monitor(second) 44 | 45 | refute first == second 46 | assert Process.alive?(second) 47 | 48 | # Ensure the last harvester has shut down 49 | assert_receive {:DOWN, _ref, _, ^second, :shutdown}, 1000 50 | end 51 | 52 | test "Ignore late reports" do 53 | TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle) 54 | 55 | harvester = 56 | Collector.Metric.HarvestCycle 57 | |> Harvest.HarvestCycle.current_harvester() 58 | 59 | assert :ok == GenServer.call(harvester, :send_harvest) 60 | 61 | GenServer.cast(harvester, {:report, :late_msg}) 62 | 63 | assert :completed == GenServer.call(harvester, :send_harvest) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/metric_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MetricTest do 2 | use ExUnit.Case 3 | 4 | test "custom metrics" do 5 | TestHelper.restart_harvest_cycle(NewRelic.Harvest.Collector.Metric.HarvestCycle) 6 | 7 | NewRelic.report_custom_metric("Foo/Bar", 100) 8 | NewRelic.report_custom_metric("Foo/Bar", 50) 9 | 10 | metrics = TestHelper.gather_harvest(NewRelic.Harvest.Collector.Metric.Harvester) 11 | 12 | [_, [count, value, _, min, max, _]] = TestHelper.find_metric(metrics, "Custom/Foo/Bar", 2) 13 | 14 | assert count == 2 15 | assert value == 150.0 16 | assert max == 100.0 17 | assert min == 50.0 18 | end 19 | 20 | test "increment custom metrics" do 21 | TestHelper.restart_harvest_cycle(NewRelic.Harvest.Collector.Metric.HarvestCycle) 22 | 23 | NewRelic.increment_custom_metric("Foo/Bar") 24 | NewRelic.increment_custom_metric("Foo/Bar", 2) 25 | 26 | metrics = TestHelper.gather_harvest(NewRelic.Harvest.Collector.Metric.Harvester) 27 | 28 | [_, [count, _, _, _, _, _]] = TestHelper.find_metric(metrics, "Custom/Foo/Bar", 3) 29 | 30 | assert count == 3 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/priority_queue_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PriorityQueueTest do 2 | use ExUnit.Case 3 | 4 | alias NewRelic.Util.PriorityQueue 5 | 6 | test "priority queue" do 7 | max = 3 8 | 9 | pq = 10 | PriorityQueue.new() 11 | |> PriorityQueue.insert(max, 2, :bar) 12 | |> PriorityQueue.insert(max, 2, :bar) 13 | |> PriorityQueue.insert(max, 2, :bar) 14 | |> PriorityQueue.insert(max, 2, :bar) 15 | |> PriorityQueue.insert(max, 3, :baz) 16 | |> PriorityQueue.insert(max, 4, :first) 17 | |> PriorityQueue.insert(max, 5, :second) 18 | |> PriorityQueue.insert(max, 5, :third) 19 | |> PriorityQueue.insert(max, 1, :foo) 20 | 21 | assert [:first, :second, :third] == PriorityQueue.values(pq) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/request_queue_time_test.exs: -------------------------------------------------------------------------------- 1 | defmodule UtilRequestStartTest do 2 | alias NewRelic.Util.RequestStart 3 | 4 | use ExUnit.Case, async: true 5 | 6 | describe "parse x-request-start header" do 7 | test "handles t={microseconds} formatted strings" do 8 | now_us = System.system_time(:microsecond) 9 | assert RequestStart.parse("t=#{now_us}") == {:ok, now_us / 1_000_000} 10 | end 11 | 12 | test "handles t={microseconds}.0 formatted strings" do 13 | now_us = System.system_time(:microsecond) 14 | assert RequestStart.parse("t=#{now_us}.0") == {:ok, now_us / 1_000_000} 15 | end 16 | 17 | test "handles t={milliseconds} formatted strings" do 18 | now_ms = System.system_time(:millisecond) 19 | assert RequestStart.parse("t=#{now_ms}") == {:ok, now_ms / 1000} 20 | end 21 | 22 | test "handles t={seconds} formatted strings" do 23 | now_s = System.system_time(:second) 24 | assert RequestStart.parse("t=#{now_s}") == {:ok, now_s} 25 | end 26 | 27 | test "handles t={fractional seconds} formatted strings" do 28 | now_us = System.system_time(:microsecond) 29 | assert RequestStart.parse("t=#{now_us / 1_000_000}") == {:ok, now_us / 1_000_000} 30 | end 31 | 32 | test "handles t={s in the future} formatted strings" do 33 | now_s = System.system_time(:second) 34 | assert {:ok, time} = RequestStart.parse("t=#{now_s + 10}") 35 | assert_in_delta time, now_s, 11 36 | end 37 | 38 | test "an invalid format is an error" do 39 | assert RequestStart.parse("nope") == :error 40 | end 41 | 42 | test "an early time is an error" do 43 | assert RequestStart.parse("t=1") == :error 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/ssl_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SSLTest do 2 | use ExUnit.Case 3 | 4 | describe "Verify SSL setup" do 5 | test "reject bad domains" do 6 | assert {:error, 7 | {:failed_connect, 8 | [ 9 | {:to_address, {~c"wrong.host.badssl.com", 443}}, 10 | {:inet, [:inet], {:tls_alert, _}} 11 | ]}} = NewRelic.Util.HTTP.post("https://wrong.host.badssl.com/", "", []) 12 | 13 | assert {:error, 14 | {:failed_connect, 15 | [ 16 | {:to_address, {~c"expired.badssl.com", 443}}, 17 | {:inet, [:inet], {:tls_alert, _}} 18 | ]}} = NewRelic.Util.HTTP.post("https://expired.badssl.com/", "", []) 19 | 20 | assert {:error, 21 | {:failed_connect, 22 | [ 23 | {:to_address, {~c"self-signed.badssl.com", 443}}, 24 | {:inet, [:inet], {:tls_alert, _}} 25 | ]}} = NewRelic.Util.HTTP.post("https://self-signed.badssl.com/", "", []) 26 | 27 | assert {:error, 28 | {:failed_connect, 29 | [ 30 | {:to_address, {~c"untrusted-root.badssl.com", 443}}, 31 | {:inet, [:inet], {:tls_alert, _}} 32 | ]}} = NewRelic.Util.HTTP.post("https://untrusted-root.badssl.com/", "", []) 33 | 34 | assert {:error, 35 | {:failed_connect, 36 | [ 37 | {:to_address, {~c"incomplete-chain.badssl.com", 443}}, 38 | {:inet, [:inet], {:tls_alert, _}} 39 | ]}} = NewRelic.Util.HTTP.post("https://incomplete-chain.badssl.com/", "", []) 40 | end 41 | 42 | test "allows good domains" do 43 | assert {:ok, _} = NewRelic.Util.HTTP.post("https://sha256.badssl.com/", "", []) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/telemetry/ecto_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Telemetry.EctoTest do 2 | use ExUnit.Case 3 | 4 | alias NewRelic.Harvest.Collector 5 | 6 | defmodule TestRepo do 7 | end 8 | 9 | # Simulate detection of an Ecto Repo 10 | setup_all do 11 | start_supervised( 12 | {NewRelic.Telemetry.Ecto, 13 | [ 14 | repo: __MODULE__.TestRepo, 15 | opts: [telemetry_prefix: [:new_relic_ecto_test]] 16 | ]} 17 | ) 18 | 19 | :ok 20 | end 21 | 22 | @event_name [:new_relic_ecto_test, :query] 23 | @measurements %{total_time: 965_000} 24 | @metadata %{ 25 | query: "SELECT i0.\"id\", i0.\"name\" FROM \"items\" AS i0", 26 | repo: __MODULE__.TestRepo, 27 | result: {:ok, %{__struct__: Postgrex.Result, command: :select}}, 28 | source: "items", 29 | type: :ecto_sql_query 30 | } 31 | test "Report expected metrics based on telemetry event" do 32 | TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle) 33 | 34 | :telemetry.execute(@event_name, @measurements, @metadata) 35 | :telemetry.execute(@event_name, @measurements, @metadata) 36 | :telemetry.execute(@event_name, @measurements, @metadata) 37 | 38 | metrics = TestHelper.gather_harvest(Collector.Metric.Harvester) 39 | 40 | assert TestHelper.find_metric( 41 | metrics, 42 | "Datastore/statement/Postgres/items/select", 43 | 3 44 | ) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/telemetry/finch_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Telemetry.FinchTest do 2 | use ExUnit.Case 3 | alias NewRelic.Harvest.Collector 4 | 5 | setup do 6 | TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle) 7 | TestHelper.restart_harvest_cycle(Collector.SpanEvent.HarvestCycle) 8 | NewRelic.DistributedTrace.BackoffSampler.reset() 9 | start_supervised({Finch, name: __MODULE__}) 10 | :ok 11 | end 12 | 13 | test "finch external metrics" do 14 | request("https://httpstat.us/200") 15 | 16 | metrics = TestHelper.gather_harvest(Collector.Metric.Harvester) 17 | 18 | assert TestHelper.find_metric(metrics, "External/httpstat.us/Finch/GET", 1) 19 | assert TestHelper.find_metric(metrics, "External/httpstat.us/all", 1) 20 | assert TestHelper.find_metric(metrics, "External/all", 1) 21 | end 22 | 23 | test "[:finch, :request, :stop] - 200" do 24 | Task.async(fn -> 25 | NewRelic.start_transaction("FinchTest", "200") 26 | request("https://httpstat.us/200") 27 | end) 28 | |> Task.await() 29 | 30 | span_events = TestHelper.gather_harvest(Collector.SpanEvent.Harvester) 31 | 32 | external_span = TestHelper.find_event(span_events, "External/httpstat.us/Finch/GET") 33 | 34 | assert external_span[:"http.url"] == "https://httpstat.us/200" 35 | assert external_span[:"http.method"] == "GET" 36 | assert external_span[:component] == "Finch" 37 | assert external_span[:"response.status"] == 200 38 | end 39 | 40 | test "[:finch, :request, :stop] - 500" do 41 | Task.async(fn -> 42 | NewRelic.start_transaction("FinchTest", "500") 43 | request("https://httpstat.us/500") 44 | end) 45 | |> Task.await() 46 | 47 | span_events = TestHelper.gather_harvest(Collector.SpanEvent.Harvester) 48 | 49 | external_span = TestHelper.find_event(span_events, "External/httpstat.us/Finch/GET") 50 | 51 | assert external_span[:"http.url"] == "https://httpstat.us/500" 52 | assert external_span[:"response.status"] == 500 53 | end 54 | 55 | test "[:finch, :request, :stop] - :error" do 56 | Task.async(fn -> 57 | NewRelic.start_transaction("FinchTest", "Error") 58 | request("https://nxdomain") 59 | end) 60 | |> Task.await() 61 | 62 | span_events = TestHelper.gather_harvest(Collector.SpanEvent.Harvester) 63 | 64 | external_span = TestHelper.find_event(span_events, "External/nxdomain/Finch/GET") 65 | 66 | assert external_span[:"http.url"] == "https://nxdomain/" 67 | assert external_span[:error] == true 68 | assert external_span[:"error.message"] |> is_binary() 69 | end 70 | 71 | @tag :capture_log 72 | test "[:finch, :request, :exception]" do 73 | {:ok, pid} = 74 | Task.start(fn -> 75 | NewRelic.start_transaction("FinchTest", "Exception") 76 | request("https://httpstat.us/200", :exception) 77 | end) 78 | 79 | Process.monitor(pid) 80 | assert_receive {:DOWN, _ref, :process, ^pid, _reason}, 1_000 81 | 82 | span_events = TestHelper.gather_harvest(Collector.SpanEvent.Harvester) 83 | 84 | external_span = TestHelper.find_event(span_events, "External/httpstat.us/Finch/GET") 85 | 86 | assert external_span[:"http.url"] == "https://httpstat.us/200" 87 | assert external_span[:error] == true 88 | assert external_span[:"error.message"] =~ "Oops" 89 | end 90 | 91 | defp request(url) do 92 | Finch.build(:get, url) 93 | |> Finch.request(__MODULE__) 94 | end 95 | 96 | defp request(url, :exception) do 97 | Finch.build(:get, url) 98 | |> Finch.stream(__MODULE__, nil, fn _, _ -> 99 | raise "Oops" 100 | end) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/telemetry_sdk/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TelemetrySdk.ConfigTest do 2 | use ExUnit.Case 3 | alias NewRelic.Harvest.TelemetrySdk 4 | 5 | test "determine correct Telemetry API hosts" do 6 | assert %{ 7 | log: "https://log-api.newrelic.com/log/v1", 8 | trace: "https://trace-api.newrelic.com/trace/v1" 9 | } = TelemetrySdk.Config.determine_hosts(nil, nil) 10 | 11 | assert %{ 12 | log: "https://log-api.eu.newrelic.com/log/v1", 13 | trace: "https://trace-api.eu.newrelic.com/trace/v1" 14 | } = TelemetrySdk.Config.determine_hosts(nil, "eu01") 15 | 16 | assert %{ 17 | log: "https://cool-log-api.newrelic.com/log/v1", 18 | trace: "https://cool-trace-api.newrelic.com/trace/v1" 19 | } = TelemetrySdk.Config.determine_hosts("cool-collector", nil) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/telemetry_sdk/dimensional_metrics_harvester_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TelemetrySdk.DimensionalMetricsHarvesterTest do 2 | use ExUnit.Case 3 | 4 | alias NewRelic.Harvest 5 | alias NewRelic.Harvest.TelemetrySdk 6 | 7 | test "Harvester collects dimensional metrics" do 8 | {:ok, harvester} = 9 | DynamicSupervisor.start_child( 10 | TelemetrySdk.DimensionalMetrics.HarvesterSupervisor, 11 | TelemetrySdk.DimensionalMetrics.Harvester 12 | ) 13 | 14 | metric1 = %{type: :gauge, name: "cpu", value: 10, attributes: %{k8: true, id: 123}} 15 | GenServer.cast(harvester, {:report, metric1}) 16 | 17 | metrics = GenServer.call(harvester, :gather_harvest) 18 | assert length(metrics) > 0 19 | end 20 | 21 | test "harvest cycle" do 22 | TestHelper.run_with(:application_config, dimensional_metrics_harvest_cycle: 300) 23 | TestHelper.restart_harvest_cycle(TelemetrySdk.DimensionalMetrics.HarvestCycle) 24 | 25 | first = Harvest.HarvestCycle.current_harvester(TelemetrySdk.DimensionalMetrics.HarvestCycle) 26 | Process.monitor(first) 27 | 28 | # Wait until harvest swap 29 | assert_receive {:DOWN, _ref, _, ^first, :shutdown}, 1000 30 | 31 | second = Harvest.HarvestCycle.current_harvester(TelemetrySdk.DimensionalMetrics.HarvestCycle) 32 | Process.monitor(second) 33 | 34 | refute first == second 35 | assert Process.alive?(second) 36 | 37 | # Ensure the last harvester has shut down 38 | assert_receive {:DOWN, _ref, _, ^second, :shutdown}, 1000 39 | end 40 | 41 | test "Ignore late reports" do 42 | TestHelper.restart_harvest_cycle(TelemetrySdk.DimensionalMetrics.HarvestCycle) 43 | 44 | harvester = 45 | TelemetrySdk.DimensionalMetrics.HarvestCycle 46 | |> Harvest.HarvestCycle.current_harvester() 47 | 48 | assert :ok == GenServer.call(harvester, :send_harvest) 49 | 50 | GenServer.cast(harvester, {:report, :late_msg}) 51 | 52 | assert :completed == GenServer.call(harvester, :send_harvest) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/telemetry_sdk/logs_harvester_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TelemetrySdk.LogsHarvesterTest do 2 | use ExUnit.Case 3 | 4 | alias NewRelic.Harvest 5 | alias NewRelic.Harvest.TelemetrySdk 6 | 7 | test "Harvester collect logs" do 8 | {:ok, harvester} = 9 | DynamicSupervisor.start_child( 10 | TelemetrySdk.Logs.HarvesterSupervisor, 11 | TelemetrySdk.Logs.Harvester 12 | ) 13 | 14 | log1 = %{} 15 | GenServer.cast(harvester, {:report, log1}) 16 | 17 | logs = GenServer.call(harvester, :gather_harvest) 18 | assert length(logs) > 0 19 | end 20 | 21 | test "harvest cycle" do 22 | TestHelper.run_with(:application_config, logs_harvest_cycle: 300) 23 | TestHelper.restart_harvest_cycle(TelemetrySdk.Logs.HarvestCycle) 24 | 25 | first = Harvest.HarvestCycle.current_harvester(TelemetrySdk.Logs.HarvestCycle) 26 | Process.monitor(first) 27 | 28 | # Wait until harvest swap 29 | assert_receive {:DOWN, _ref, _, ^first, :shutdown}, 1000 30 | 31 | second = Harvest.HarvestCycle.current_harvester(TelemetrySdk.Logs.HarvestCycle) 32 | Process.monitor(second) 33 | 34 | refute first == second 35 | assert Process.alive?(second) 36 | 37 | # Ensure the last harvester has shut down 38 | assert_receive {:DOWN, _ref, _, ^second, :shutdown}, 1000 39 | end 40 | 41 | test "Ignore late reports" do 42 | TestHelper.restart_harvest_cycle(TelemetrySdk.Logs.HarvestCycle) 43 | 44 | harvester = 45 | TelemetrySdk.Logs.HarvestCycle 46 | |> Harvest.HarvestCycle.current_harvester() 47 | 48 | assert :ok == GenServer.call(harvester, :send_harvest) 49 | 50 | GenServer.cast(harvester, {:report, :late_msg}) 51 | 52 | assert :completed == GenServer.call(harvester, :send_harvest) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/telemetry_sdk/span_harvester_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TelemetrySdk.SpanHarvesterTest do 2 | use ExUnit.Case 3 | 4 | alias NewRelic.Harvest 5 | alias NewRelic.Harvest.TelemetrySdk 6 | 7 | test "Harvester collect spans" do 8 | {:ok, harvester} = 9 | DynamicSupervisor.start_child( 10 | TelemetrySdk.Spans.HarvesterSupervisor, 11 | TelemetrySdk.Spans.Harvester 12 | ) 13 | 14 | span1 = %{} 15 | GenServer.cast(harvester, {:report, span1}) 16 | 17 | spans = GenServer.call(harvester, :gather_harvest) 18 | assert length(spans) > 0 19 | end 20 | 21 | test "harvest cycle" do 22 | TestHelper.run_with(:application_config, spans_harvest_cycle: 300) 23 | TestHelper.restart_harvest_cycle(TelemetrySdk.Spans.HarvestCycle) 24 | 25 | first = Harvest.HarvestCycle.current_harvester(TelemetrySdk.Spans.HarvestCycle) 26 | Process.monitor(first) 27 | 28 | # Wait until harvest swap 29 | assert_receive {:DOWN, _ref, _, ^first, :shutdown}, 1000 30 | 31 | second = Harvest.HarvestCycle.current_harvester(TelemetrySdk.Spans.HarvestCycle) 32 | Process.monitor(second) 33 | 34 | refute first == second 35 | assert Process.alive?(second) 36 | 37 | # Ensure the last harvester has shut down 38 | assert_receive {:DOWN, _ref, _, ^second, :shutdown}, 1000 39 | end 40 | 41 | test "Ignore late reports" do 42 | TestHelper.restart_harvest_cycle(TelemetrySdk.Spans.HarvestCycle) 43 | 44 | harvester = 45 | TelemetrySdk.Spans.HarvestCycle 46 | |> Harvest.HarvestCycle.current_harvester() 47 | 48 | assert :ok == GenServer.call(harvester, :send_harvest) 49 | 50 | GenServer.cast(harvester, {:report, :late_msg}) 51 | 52 | assert :completed == GenServer.call(harvester, :send_harvest) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Application.ensure_all_started(:plug) 2 | 3 | unless System.get_env("NR_INT_TEST") do 4 | {:ok, _} = NewRelic.EnabledSupervisor.start_link(:ok) 5 | end 6 | 7 | ExUnit.start() 8 | 9 | System.at_exit(fn _ -> 10 | IO.puts(GenServer.call(NewRelic.Logger, :flush)) 11 | end) 12 | -------------------------------------------------------------------------------- /test/tracer_macro_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NewRelic.Tracer.MacroTest do 2 | use ExUnit.Case 3 | 4 | @doc """ 5 | We re-inject the function args into the call to the Tracer reporter 6 | without generating a bunch of unused variable warnings. 7 | """ 8 | describe "build_call_args/1" do 9 | test "do nothing to simple argument lists" do 10 | ast = 11 | quote do 12 | [a, b, 100, [h | t]] 13 | end 14 | 15 | assert ast == NewRelic.Tracer.Macro.build_call_args(ast) 16 | end 17 | 18 | test "substitute ignored variables with an atom" do 19 | ast = 20 | quote do 21 | [a, _ignored_val, b] 22 | end 23 | 24 | expected = 25 | quote do 26 | [a, :__ignored__, b] 27 | end 28 | 29 | assert expected == NewRelic.Tracer.Macro.build_call_args(ast) 30 | end 31 | 32 | test "create a proper list when ignoring tail of a list" do 33 | ast = 34 | quote do 35 | [a | _ignored] 36 | end 37 | 38 | expected = 39 | quote do 40 | [a | []] 41 | end 42 | 43 | assert expected == NewRelic.Tracer.Macro.build_call_args(ast) 44 | end 45 | 46 | test "strip default values" do 47 | ast = 48 | quote do 49 | [a \\ 100] 50 | end 51 | 52 | expected = 53 | quote do 54 | [a] 55 | end 56 | 57 | assert expected == NewRelic.Tracer.Macro.build_call_args(ast) 58 | end 59 | 60 | test "Drop the de-structuring in favor of the variable" do 61 | ast = 62 | quote do 63 | [ 64 | %{v1: v1, v2: v2, v3: %{foo: bar} = v3} = data, 65 | x = y, 66 | [[hh, hhh] = h | tail] = lst 67 | ] 68 | end 69 | 70 | expected = 71 | quote do 72 | [ 73 | data, 74 | y, 75 | lst 76 | ] 77 | end 78 | 79 | assert expected == NewRelic.Tracer.Macro.build_call_args(ast) 80 | end 81 | 82 | test "Find variable on the left of a pattern match" do 83 | ast = 84 | quote do 85 | [data = %{foo: %{baz: "qux"}}] 86 | end 87 | 88 | expected = 89 | quote do 90 | [data] 91 | end 92 | 93 | assert expected == NewRelic.Tracer.Macro.build_call_args(ast) 94 | end 95 | 96 | test "Handle a strange double-sided pattern match" do 97 | ast = 98 | quote do 99 | [data = %{foo: %{baz: "qux"}} = map] 100 | end 101 | 102 | expected = 103 | quote do 104 | [map] 105 | end 106 | 107 | assert expected == NewRelic.Tracer.Macro.build_call_args(ast) 108 | end 109 | 110 | test "Handle a struct with enforced_keys" do 111 | ast = 112 | quote do 113 | [%NaiveDateTime{year: year}] 114 | end 115 | 116 | expected = 117 | quote do 118 | [%{__struct__: NaiveDateTime, year: year}] 119 | end 120 | 121 | assert expected == NewRelic.Tracer.Macro.build_call_args(ast) 122 | end 123 | end 124 | end 125 | --------------------------------------------------------------------------------