Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ecto adapter attribute #65

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ jobs:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: opentelemetry_ecto_test
mariadb:
image: mariadb:11.0
ports: ['3306:3306']
options: --health-cmd "healthcheck.sh --connect --innodb_initialized" --health-interval 10s --health-timeout 5s --health-retries 5
env:
MARIADB_RANDOM_ROOT_PASSWORD: "yes"
MARIADB_USER: mariadb
MARIADB_PASSWORD: mariadb
MARIADB_DATABASE: opentelemetry_ecto_test
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
Expand Down
16 changes: 14 additions & 2 deletions instrumentation/opentelemetry_ecto/config/test.exs
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import Config

config :opentelemetry_ecto,
ecto_repos: [OpentelemetryEcto.TestRepo]
ecto_repos: [
OpentelemetryEcto.MariaDBTestRepo,
OpentelemetryEcto.TestRepo
]

config :opentelemetry_ecto, OpentelemetryEcto.TestRepo,
hostname: "localhost",
username: "postgres",
password: "postgres",
database: "opentelemetry_ecto_test",
pool: Ecto.Adapters.SQL.Sandbox
pool: Ecto.Adapters.SQL.Sandbox,
telemetry_prefix: [:opentelemetry_ecto, :test_repo]

config :opentelemetry_ecto, OpentelemetryEcto.MariaDBTestRepo,
hostname: "localhost",
username: "mariadb",
password: "mariadb",
database: "opentelemetry_ecto_test",
pool: Ecto.Adapters.SQL.Sandbox,
telemetry_prefix: [:opentelemetry_ecto, :mariadb_test_repo]

config :opentelemetry,
processors: [{:otel_batch_processor, %{scheduled_delay_ms: 1}}]
10 changes: 10 additions & 0 deletions instrumentation/opentelemetry_ecto/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ services:
- POSTGRES_DB=opentelemetry_ecto_test
ports:
- 5432:5432

mariadb:
image: mariadb:11.0
environment:
- MARIADB_RANDOM_ROOT_PASSWORD=yes
- MARIADB_USER=mariadb
- MARIADB_PASSWORD=mariadb
- MARIADB_DATABASE=opentelemetry_ecto_test
ports:
- 3306:3306
114 changes: 72 additions & 42 deletions instrumentation/opentelemetry_ecto/lib/opentelemetry_ecto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,61 @@ defmodule OpentelemetryEcto do

require OpenTelemetry.Tracer

@db_systems [
"other_sql",
"mssql",
"mssqlcompact",
"mysql",
"oracle",
"db2",
"postgresql",
"redshift",
"hive",
"cloudscape",
"hsqldb",
"progress",
"maxdb",
"hanadb",
"ingres",
"firstsql",
"edb",
"cache",
"adabas",
"firebird",
"derby",
"filemaker",
"informix",
"instantdb",
"interbase",
"mariadb",
"netezza",
"pervasive",
"pointbase",
"sqlite",
"sybase",
"teradata",
"vertica",
"h2",
"coldfusion",
"cassandra",
"hbase",
"mongodb",
"redis",
"couchbase",
"couchdb",
"cosmosdb",
"dynamodb",
"neo4j",
"geode",
"elasticsearch",
"memcached",
"cockroachdb",
"opensearch",
"clickhouse",
"spanner",
"trino"
]

@doc """
Attaches the OpentelemetryEcto handler to your repo events. This should be called
from your application behaviour on startup.
Expand All @@ -44,6 +99,10 @@ defmodule OpentelemetryEcto do
sanitized version of it. This is useful for removing sensitive information from the
query string. Unless this option is `:enabled` or a function,
query statements will not be recorded on spans.
* `:db_system` - The identifier for the database management system (DBMS).
defaults to the mapped value of the ecto adapter used.
Must follow the list of well-known db systems from semantic conventions.
See `https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md`
Comment on lines +102 to +105
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be globally or per Adapter bases?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per adapter bases. I think OpentelemetryEcto.setup/2 is meant to use just for one repo, right? And each repo can have only one Adapter (AFAIK).

"""
def setup(event_prefix, config \\ []) do
event = event_prefix ++ [:query]
Expand All @@ -54,7 +113,7 @@ defmodule OpentelemetryEcto do
def handle_event(
event,
measurements,
%{query: query, source: source, result: query_result, repo: repo, type: type},
%{query: query, source: source, result: query_result, repo: repo},
config
) do
# Doing all this even if the span isn't sampled so the sampler
Expand All @@ -64,16 +123,7 @@ defmodule OpentelemetryEcto do
end_time = :opentelemetry.timestamp()
start_time = end_time - total_time
database = repo.config()[:database]

url =
case repo.config()[:url] do
nil ->
# TODO: add port
URI.to_string(%URI{scheme: "ecto", host: repo.config()[:hostname]})

url ->
url
end
adapter = repo.__adapter__()

span_prefix =
case Keyword.fetch(config, :span_prefix) do
Expand All @@ -87,20 +137,13 @@ defmodule OpentelemetryEcto do
time_unit = Keyword.get(config, :time_unit, :microsecond)
additional_attributes = Keyword.get(config, :additional_attributes, %{})

db_type =
case type do
:ecto_sql_query -> :sql
_ -> type
end

# TODO: need connection information to complete the required attributes
# net.peer.name or net.peer.ip and net.peer.port
base_attributes = %{
"db.type": db_type,
source: source,
"db.instance": database,
"ecto.db.adapter": to_string(adapter),
"db.system": db_system(config[:db_system], adapter),
"db.name": database,
"db.url": url,
"db.sql.table": source,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't actually know that an Ecto source is a sql table, do we here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://hexdocs.pm/ecto/Ecto.Schema.Metadata.html#module-source

The doc says it will be a table or a collection, I don't know if we have a way to determine if it's a table, but it could be a table. I'm not sure if for SQL adapters it will be always a table.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figure for a sql adapter it will alway be a table. So thats all we need to know to know to include it (I think).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be a view, for example, not sure if it would break your semantics.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's breaks the semantics here, view is like a virtual table.

"total_time_#{time_unit}s": System.convert_time_unit(total_time, :native, time_unit)
}

Expand All @@ -110,7 +153,6 @@ defmodule OpentelemetryEcto do
base_attributes
|> add_measurements(measurements, time_unit)
|> maybe_add_db_statement(db_statement_config, query)
|> maybe_add_db_system(repo.__adapter__())
|> add_additional_attributes(additional_attributes)

parent_context =
Expand Down Expand Up @@ -189,27 +231,15 @@ defmodule OpentelemetryEcto do
attributes
end

defp maybe_add_db_system(attributes, Ecto.Adapters.Postgres) do
Map.put(attributes, :"db.system", :postgresql)
end

defp maybe_add_db_system(attributes, Ecto.Adapters.MyXQL) do
Map.put(attributes, :"db.system", :mysql)
end

defp maybe_add_db_system(attributes, Ecto.Adapters.SQLite3) do
Map.put(attributes, :"db.system", :sqlite)
end

defp maybe_add_db_system(attributes, Ecto.Adapters.Tds) do
Map.put(attributes, :"db.system", :mssql)
end

defp maybe_add_db_system(attributes, _) do
attributes
end

defp add_additional_attributes(attributes, additional_attributes) do
Map.merge(attributes, additional_attributes)
end

defp db_system(db_system) when db_system in @db_systems, do: db_system
defp db_system(_), do: "other_sql"

defp db_system(nil, Ecto.Adapters.Postgres), do: "postgresql"
defp db_system(nil, Ecto.Adapters.MyXQL), do: "mysql"
defp db_system(nil, Ecto.Adapters.Tds), do: "mssql"
defp db_system(db_system, _), do: db_system(db_system)
Comment on lines +237 to +244
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tsloughter leaving a note here, as I said before, it will be prudent to ask Ecto folks to include a function under the adapter module that allow us to fetch the engine name ...

I am not sure how open they will be, but I think it is a fair thing to ask and see what happens.

"Ideally," this package wouldn't maintain the mapping

end
1 change: 1 addition & 0 deletions instrumentation/opentelemetry_ecto/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ defmodule OpentelemetryEcto.MixProject do
{:ex_doc, "~> 0.29", only: [:dev], runtime: false},
{:ecto_sql, ">= 3.0.0", only: [:dev, :test]},
{:postgrex, ">= 0.15.0", only: [:dev, :test]},
{:myxql, "~> 0.6.0", only: [:dev, :test]},
{:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false},
{:opentelemetry_process_propagator, "~> 0.2"}
]
Expand Down
1 change: 1 addition & 0 deletions instrumentation/opentelemetry_ecto/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"myxql": {:hex, :myxql, "0.6.1", "ed97d2af9e15c7da7ef7df1a29b2d0d4194444cbb146843715fab9f098c78de2", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "cc34c3e6d5151d09175c107bd45d99865c26d149e29fc5a78a2f1b799325d76d"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"opentelemetry": {:hex, :opentelemetry, "1.3.0", "988ac3c26acac9720a1d4fb8d9dc52e95b45ecfec2d5b5583276a09e8936bc5e", [:rebar3], [{:opentelemetry_api, "~> 1.2.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "8e09edc26aad11161509d7ecad854a3285d88580f93b63b0b1cf0bac332bfcc0"},
"opentelemetry_api": {:hex, :opentelemetry_api, "1.2.1", "7b69ed4f40025c005de0b74fce8c0549625d59cb4df12d15c32fe6dc5076ff42", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "6d7a27b7cad2ad69a09cabf6670514cafcec717c8441beb5c96322bac3d05350"},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule OpentelemetryEcto.MariaDBTestRepo.Migrations.SetupTables do
use Ecto.Migration

def change do
create table(:users) do
add :email, :string
end

create table(:posts) do
add :body, :text
add :user_id, references(:users)
end

create table(:comments) do
add :body, :text
add :user_id, references(:users)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule OpentelemetryEctoTest do
import Ecto.Query
require OpenTelemetry.Tracer

alias OpentelemetryEcto.MariaDBTestRepo, as: MariaDBRepo
alias OpentelemetryEcto.TestRepo, as: Repo
alias OpentelemetryEcto.TestModels.{Comment, User, Post}

Expand Down Expand Up @@ -49,14 +50,13 @@ defmodule OpentelemetryEctoTest do
)}

assert %{
"db.system": :postgresql,
"db.instance": "opentelemetry_ecto_test",
"db.type": :sql,
"db.url": "ecto://localhost",
"db.name": "opentelemetry_ecto_test",
"db.sql.table": "users",
"db.system": "postgresql",
"ecto.db.adapter": "Elixir.Ecto.Adapters.Postgres",
decode_time_microseconds: _,
query_time_microseconds: _,
queue_time_microseconds: _,
source: "users",
total_time_microseconds: _
} = :otel_attributes.map(attributes)
end
Expand Down Expand Up @@ -112,14 +112,13 @@ defmodule OpentelemetryEctoTest do
)}

assert %{
"db.system": :postgresql,
"db.instance": "opentelemetry_ecto_test",
"db.type": :sql,
"db.url": "ecto://localhost",
"db.name": "opentelemetry_ecto_test",
"db.sql.table": "posts",
"db.system": "postgresql",
"ecto.db.adapter": "Elixir.Ecto.Adapters.Postgres",
decode_time_milliseconds: _,
query_time_milliseconds: _,
queue_time_milliseconds: _,
source: "posts",
total_time_milliseconds: _
} = :otel_attributes.map(attributes)
end
Expand Down Expand Up @@ -305,11 +304,45 @@ defmodule OpentelemetryEctoTest do
)}
end

def attach_handler(config \\ []) do
test "changes the db system" do
attach_handler([db_system: "mariadb"], [:opentelemetry_ecto, :mariadb_test_repo])

MariaDBRepo.all(User)

assert_receive {:span,
span(
name: "opentelemetry_ecto.mariadb_test_repo.query:users",
attributes: attributes
)}

assert %{
"ecto.db.adapter": "Elixir.Ecto.Adapters.MyXQL",
"db.system": "mariadb"
} = :otel_attributes.map(attributes)
end

test "fallbacks to default if it is not a well-known db systems" do
attach_handler([db_system: "maria_db"], [:opentelemetry_ecto, :mariadb_test_repo])

MariaDBRepo.all(User)

assert_receive {:span,
span(
name: "opentelemetry_ecto.mariadb_test_repo.query:users",
attributes: attributes
)}

assert %{
"ecto.db.adapter": "Elixir.Ecto.Adapters.MyXQL",
"db.system": "other_sql"
} = :otel_attributes.map(attributes)
end

def attach_handler(config \\ [], event_name \\ @event_name) do
# For now setup the handler manually in each test
handler = {__MODULE__, self()}

:telemetry.attach(handler, @event_name ++ [:query], &OpentelemetryEcto.handle_event/4, config)
:telemetry.attach(handler, event_name ++ [:query], &OpentelemetryEcto.handle_event/4, config)

on_exit(fn ->
:telemetry.detach(handler)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule OpentelemetryEcto.MariaDBTestRepo do
use Ecto.Repo,
otp_app: :opentelemetry_ecto,
adapter: Ecto.Adapters.MyXQL
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
defmodule OpentelemetryEcto.TestRepo do
use Ecto.Repo,
otp_app: :opentelemetry_ecto,
adapter: Ecto.Adapters.Postgres,
telemetry_prefix: [:opentelemetry_ecto, :test_repo]
adapter: Ecto.Adapters.Postgres
end
2 changes: 2 additions & 0 deletions instrumentation/opentelemetry_ecto/test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
OpentelemetryEcto.MariaDBTestRepo.start_link()
OpentelemetryEcto.TestRepo.start_link()

ExUnit.start(capture_log: true)

Ecto.Adapters.SQL.Sandbox.mode(OpentelemetryEcto.MariaDBTestRepo, {:shared, self()})
Ecto.Adapters.SQL.Sandbox.mode(OpentelemetryEcto.TestRepo, {:shared, self()})