├── .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 |
--------------------------------------------------------------------------------