diff --git a/telemetry_api/.formatter.exs b/telemetry_api/.formatter.exs new file mode 100644 index 000000000..5971023f6 --- /dev/null +++ b/telemetry_api/.formatter.exs @@ -0,0 +1,5 @@ +[ + import_deps: [:ecto, :ecto_sql, :phoenix], + subdirectories: ["priv/*/migrations"], + inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}", "priv/*/seeds.exs"] +] diff --git a/telemetry_api/.gitignore b/telemetry_api/.gitignore new file mode 100644 index 000000000..697102350 --- /dev/null +++ b/telemetry_api/.gitignore @@ -0,0 +1,27 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Temporary files, for example, from tests. +/tmp/ + +# Ignore package tarball (built via "mix hex.build"). +telemetry_api-*.tar + diff --git a/telemetry_api/Dockerfile b/telemetry_api/Dockerfile new file mode 100644 index 000000000..0cf033f72 --- /dev/null +++ b/telemetry_api/Dockerfile @@ -0,0 +1,10 @@ +# https://hub.docker.com/_/postgres +FROM postgres:16.3 + +# Environment variables +ENV POSTGRES_USER=telemetry_user +ENV POSTGRES_PASSWORD=telemetry_pass +ENV POSTGRES_DB=telemetry_db + +# Expose the default PostgreSQL port +EXPOSE 5432 diff --git a/telemetry_api/README.md b/telemetry_api/README.md new file mode 100644 index 000000000..63ebe6d1c --- /dev/null +++ b/telemetry_api/README.md @@ -0,0 +1,18 @@ +# TelemetryApi + +To start your Phoenix server: + + * Run `mix setup` to install and setup dependencies + * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` + +Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. + +Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). + +## Learn more + + * Official website: https://www.phoenixframework.org/ + * Guides: https://hexdocs.pm/phoenix/overview.html + * Docs: https://hexdocs.pm/phoenix + * Forum: https://elixirforum.com/c/phoenix-forum + * Source: https://github.com/phoenixframework/phoenix diff --git a/telemetry_api/config/config.exs b/telemetry_api/config/config.exs new file mode 100644 index 000000000..46bf3bd9c --- /dev/null +++ b/telemetry_api/config/config.exs @@ -0,0 +1,35 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Config module. +# +# This configuration file is loaded before any dependency and +# is restricted to this project. + +# General application configuration +import Config + +config :telemetry_api, + ecto_repos: [TelemetryApi.Repo], + generators: [timestamp_type: :utc_datetime] + +# Configures the endpoint +config :telemetry_api, TelemetryApiWeb.Endpoint, + url: [host: "localhost"], + adapter: Bandit.PhoenixAdapter, + render_errors: [ + formats: [json: TelemetryApiWeb.ErrorJSON], + layout: false + ], + pubsub_server: TelemetryApi.PubSub, + live_view: [signing_salt: "eQaI7lMW"] + +# Configures Elixir's Logger +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +# Use Jason for JSON parsing in Phoenix +config :phoenix, :json_library, Jason + +# Import environment specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +import_config "#{config_env()}.exs" diff --git a/telemetry_api/config/dev.exs b/telemetry_api/config/dev.exs new file mode 100644 index 000000000..90fd36b6f --- /dev/null +++ b/telemetry_api/config/dev.exs @@ -0,0 +1,64 @@ +import Config + +# Configure your database +config :telemetry_api, TelemetryApi.Repo, + username: "telemetry_user", + password: "telemetry_pass", + database: "telemetry_db", + hostname: "localhost", + port: 5432, + stacktrace: true, + show_sensitive_data_on_connection_error: true, + pool_size: 10 + +# For development, we disable any cache and enable +# debugging and code reloading. +# +# The watchers configuration can be used to run external +# watchers to your application. For example, we can use it +# to bundle .js and .css sources. +config :telemetry_api, TelemetryApiWeb.Endpoint, + # Binding to loopback ipv4 address prevents access from other machines. + # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. + http: [ip: {127, 0, 0, 1}, port: 4000], + check_origin: false, + code_reloader: true, + debug_errors: true, + secret_key_base: "SKxKWZhd8cfUXMUqskUHvegw8P46kkfwIYWTW86tsqn+t6M2S1HUFjTqgVWAkvX0", + watchers: [] + +# ## SSL Support +# +# In order to use HTTPS in development, a self-signed +# certificate can be generated by running the following +# Mix task: +# +# mix phx.gen.cert +# +# Run `mix help phx.gen.cert` for more information. +# +# The `http:` config above can be replaced with: +# +# https: [ +# port: 4001, +# cipher_suite: :strong, +# keyfile: "priv/cert/selfsigned_key.pem", +# certfile: "priv/cert/selfsigned.pem" +# ], +# +# If desired, both `http:` and `https:` keys can be +# configured to run both http and https servers on +# different ports. + +# Enable dev routes for dashboard and mailbox +config :telemetry_api, dev_routes: true + +# Do not include metadata nor timestamps in development logs +config :logger, :console, format: "[$level] $message\n" + +# Set a higher stacktrace during development. Avoid configuring such +# in production as building large stacktraces may be expensive. +config :phoenix, :stacktrace_depth, 20 + +# Initialize plugs at runtime for faster development compilation +config :phoenix, :plug_init_mode, :runtime diff --git a/telemetry_api/config/prod.exs b/telemetry_api/config/prod.exs new file mode 100644 index 000000000..1fe2d9e85 --- /dev/null +++ b/telemetry_api/config/prod.exs @@ -0,0 +1,7 @@ +import Config + +# Do not print debug messages in production +config :logger, level: :info + +# Runtime production configuration, including reading +# of environment variables, is done on config/runtime.exs. diff --git a/telemetry_api/config/runtime.exs b/telemetry_api/config/runtime.exs new file mode 100644 index 000000000..0d07d01e1 --- /dev/null +++ b/telemetry_api/config/runtime.exs @@ -0,0 +1,99 @@ +import Config + +# config/runtime.exs is executed for all environments, including +# during releases. It is executed after compilation and before the +# system starts, so it is typically used to load production configuration +# and secrets from environment variables or elsewhere. Do not define +# any compile-time configuration in here, as it won't be applied. +# The block below contains prod specific runtime configuration. + +# ## Using releases +# +# If you use `mix release`, you need to explicitly enable the server +# by passing the PHX_SERVER=true when you start it: +# +# PHX_SERVER=true bin/telemetry_api start +# +# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` +# script that automatically sets the env var above. +if System.get_env("PHX_SERVER") do + config :telemetry_api, TelemetryApiWeb.Endpoint, server: true +end + +if config_env() == :prod do + database_url = + System.get_env("DATABASE_URL") || + raise """ + environment variable DATABASE_URL is missing. + For example: ecto://USER:PASS@HOST/DATABASE + """ + + maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] + + config :telemetry_api, TelemetryApi.Repo, + # ssl: true, + url: database_url, + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), + socket_options: maybe_ipv6 + + # The secret key base is used to sign/encrypt cookies and other secrets. + # A default value is used in config/dev.exs and config/test.exs but you + # want to use a different value for prod and you most likely don't want + # to check this value into version control, so we use an environment + # variable instead. + secret_key_base = + System.get_env("SECRET_KEY_BASE") || + raise """ + environment variable SECRET_KEY_BASE is missing. + You can generate one by calling: mix phx.gen.secret + """ + + host = System.get_env("PHX_HOST") || "example.com" + port = String.to_integer(System.get_env("PORT") || "4000") + + config :telemetry_api, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") + + config :telemetry_api, TelemetryApiWeb.Endpoint, + url: [host: host, port: 443, scheme: "https"], + http: [ + # Enable IPv6 and bind on all interfaces. + # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. + # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 + # for details about using IPv6 vs IPv4 and loopback vs public addresses. + ip: {0, 0, 0, 0, 0, 0, 0, 0}, + port: port + ], + secret_key_base: secret_key_base + + # ## SSL Support + # + # To get SSL working, you will need to add the `https` key + # to your endpoint configuration: + # + # config :telemetry_api, TelemetryApiWeb.Endpoint, + # https: [ + # ..., + # port: 443, + # cipher_suite: :strong, + # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), + # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") + # ] + # + # The `cipher_suite` is set to `:strong` to support only the + # latest and more secure SSL ciphers. This means old browsers + # and clients may not be supported. You can set it to + # `:compatible` for wider support. + # + # `:keyfile` and `:certfile` expect an absolute path to the key + # and cert in disk or a relative path inside priv, for example + # "priv/ssl/server.key". For all supported SSL configuration + # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 + # + # We also recommend setting `force_ssl` in your config/prod.exs, + # ensuring no data is ever sent via http, always redirecting to https: + # + # config :telemetry_api, TelemetryApiWeb.Endpoint, + # force_ssl: [hsts: true] + # + # Check `Plug.SSL` for all available options in `force_ssl`. +end diff --git a/telemetry_api/config/test.exs b/telemetry_api/config/test.exs new file mode 100644 index 000000000..5916d25d5 --- /dev/null +++ b/telemetry_api/config/test.exs @@ -0,0 +1,27 @@ +import Config + +# Configure your database +# +# The MIX_TEST_PARTITION environment variable can be used +# to provide built-in test partitioning in CI environment. +# Run `mix help test` for more information. +config :telemetry_api, TelemetryApi.Repo, + username: "postgres", + password: "postgres", + hostname: "localhost", + database: "telemetry_api_test#{System.get_env("MIX_TEST_PARTITION")}", + pool: Ecto.Adapters.SQL.Sandbox, + pool_size: System.schedulers_online() * 2 + +# We don't run a server during test. If one is required, +# you can enable the server option below. +config :telemetry_api, TelemetryApiWeb.Endpoint, + http: [ip: {127, 0, 0, 1}, port: 4002], + secret_key_base: "oxlWGXR4lI6jsDZfizxmFwhPFn9vjc6rWqsdAnu5rlTmluTstw3/6YBkB5OGKi5m", + server: false + +# Print only warnings and errors during test +config :logger, level: :warning + +# Initialize plugs at runtime for faster test compilation +config :phoenix, :plug_init_mode, :runtime diff --git a/telemetry_api/lib/telemetry_api.ex b/telemetry_api/lib/telemetry_api.ex new file mode 100644 index 000000000..6d8157c74 --- /dev/null +++ b/telemetry_api/lib/telemetry_api.ex @@ -0,0 +1,9 @@ +defmodule TelemetryApi do + @moduledoc """ + TelemetryApi keeps the contexts that define your domain + and business logic. + + Contexts are also responsible for managing your data, regardless + if it comes from the database, an external API or others. + """ +end diff --git a/telemetry_api/lib/telemetry_api/application.ex b/telemetry_api/lib/telemetry_api/application.ex new file mode 100644 index 000000000..d1a065189 --- /dev/null +++ b/telemetry_api/lib/telemetry_api/application.ex @@ -0,0 +1,34 @@ +defmodule TelemetryApi.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + TelemetryApiWeb.Telemetry, + TelemetryApi.Repo, + {DNSCluster, query: Application.get_env(:telemetry_api, :dns_cluster_query) || :ignore}, + {Phoenix.PubSub, name: TelemetryApi.PubSub}, + # Start a worker by calling: TelemetryApi.Worker.start_link(arg) + # {TelemetryApi.Worker, arg}, + # Start to serve requests, typically the last entry + TelemetryApiWeb.Endpoint + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: TelemetryApi.Supervisor] + Supervisor.start_link(children, opts) + end + + # Tell Phoenix to update the endpoint configuration + # whenever the application is updated. + @impl true + def config_change(changed, _new, removed) do + TelemetryApiWeb.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/telemetry_api/lib/telemetry_api/operators.ex b/telemetry_api/lib/telemetry_api/operators.ex new file mode 100644 index 000000000..52a834ded --- /dev/null +++ b/telemetry_api/lib/telemetry_api/operators.ex @@ -0,0 +1,104 @@ +defmodule TelemetryApi.Operators do + @moduledoc """ + The Operators context. + """ + + import Ecto.Query, warn: false + alias TelemetryApi.Repo + + alias TelemetryApi.Operators.Operator + + @doc """ + Returns the list of operators. + + ## Examples + + iex> list_operators() + [%Operator{}, ...] + + """ + def list_operators do + Repo.all(Operator) + end + + @doc """ + Gets a single operator. + + Raises `Ecto.NoResultsError` if the Operator does not exist. + + ## Examples + + iex> get_operator!(123) + %Operator{} + + iex> get_operator!(456) + ** (Ecto.NoResultsError) + + """ + def get_operator!(id), do: Repo.get!(Operator, id) + + @doc """ + Creates a operator. + + ## Examples + + iex> create_operator(%{field: value}) + {:ok, %Operator{}} + + iex> create_operator(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_operator(attrs \\ %{}) do + %Operator{} + |> Operator.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a operator. + + ## Examples + + iex> update_operator(operator, %{field: new_value}) + {:ok, %Operator{}} + + iex> update_operator(operator, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_operator(%Operator{} = operator, attrs) do + operator + |> Operator.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a operator. + + ## Examples + + iex> delete_operator(operator) + {:ok, %Operator{}} + + iex> delete_operator(operator) + {:error, %Ecto.Changeset{}} + + """ + def delete_operator(%Operator{} = operator) do + Repo.delete(operator) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking operator changes. + + ## Examples + + iex> change_operator(operator) + %Ecto.Changeset{data: %Operator{}} + + """ + def change_operator(%Operator{} = operator, attrs \\ %{}) do + Operator.changeset(operator, attrs) + end +end diff --git a/telemetry_api/lib/telemetry_api/operators/operator.ex b/telemetry_api/lib/telemetry_api/operators/operator.ex new file mode 100644 index 000000000..4bdc7d195 --- /dev/null +++ b/telemetry_api/lib/telemetry_api/operators/operator.ex @@ -0,0 +1,18 @@ +defmodule TelemetryApi.Operators.Operator do + use Ecto.Schema + import Ecto.Changeset + + schema "operators" do + field :version, :string + field :address, :string + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(operator, attrs) do + operator + |> cast(attrs, [:address, :version]) + |> validate_required([:address, :version]) + end +end diff --git a/telemetry_api/lib/telemetry_api/repo.ex b/telemetry_api/lib/telemetry_api/repo.ex new file mode 100644 index 000000000..8f57bf72e --- /dev/null +++ b/telemetry_api/lib/telemetry_api/repo.ex @@ -0,0 +1,5 @@ +defmodule TelemetryApi.Repo do + use Ecto.Repo, + otp_app: :telemetry_api, + adapter: Ecto.Adapters.Postgres +end diff --git a/telemetry_api/lib/telemetry_api_web.ex b/telemetry_api/lib/telemetry_api_web.ex new file mode 100644 index 000000000..90b61b603 --- /dev/null +++ b/telemetry_api/lib/telemetry_api_web.ex @@ -0,0 +1,65 @@ +defmodule TelemetryApiWeb do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, components, channels, and so on. + + This can be used in your application as: + + use TelemetryApiWeb, :controller + use TelemetryApiWeb, :html + + The definitions below will be executed for every controller, + component, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define additional modules and import + those modules here. + """ + + def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) + + def router do + quote do + use Phoenix.Router, helpers: false + + # Import common connection and controller functions to use in pipelines + import Plug.Conn + import Phoenix.Controller + end + end + + def channel do + quote do + use Phoenix.Channel + end + end + + def controller do + quote do + use Phoenix.Controller, + formats: [:html, :json], + layouts: [html: TelemetryApiWeb.Layouts] + + import Plug.Conn + + unquote(verified_routes()) + end + end + + def verified_routes do + quote do + use Phoenix.VerifiedRoutes, + endpoint: TelemetryApiWeb.Endpoint, + router: TelemetryApiWeb.Router, + statics: TelemetryApiWeb.static_paths() + end + end + + @doc """ + When used, dispatch to the appropriate controller/live_view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/telemetry_api/lib/telemetry_api_web/controllers/changeset_json.ex b/telemetry_api/lib/telemetry_api_web/controllers/changeset_json.ex new file mode 100644 index 000000000..5085846f9 --- /dev/null +++ b/telemetry_api/lib/telemetry_api_web/controllers/changeset_json.ex @@ -0,0 +1,25 @@ +defmodule TelemetryApiWeb.ChangesetJSON do + @doc """ + Renders changeset errors. + """ + def error(%{changeset: changeset}) do + # When encoded, the changeset returns its errors + # as a JSON object. So we just pass it forward. + %{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)} + end + + defp translate_error({msg, opts}) do + # You can make use of gettext to translate error messages by + # uncommenting and adjusting the following code: + + # if count = opts[:count] do + # Gettext.dngettext(TelemetryApiWeb.Gettext, "errors", msg, msg, count, opts) + # else + # Gettext.dgettext(TelemetryApiWeb.Gettext, "errors", msg, opts) + # end + + Enum.reduce(opts, msg, fn {key, value}, acc -> + String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) + end) + end +end diff --git a/telemetry_api/lib/telemetry_api_web/controllers/error_json.ex b/telemetry_api/lib/telemetry_api_web/controllers/error_json.ex new file mode 100644 index 000000000..6c619a972 --- /dev/null +++ b/telemetry_api/lib/telemetry_api_web/controllers/error_json.ex @@ -0,0 +1,21 @@ +defmodule TelemetryApiWeb.ErrorJSON do + @moduledoc """ + This module is invoked by your endpoint in case of errors on JSON requests. + + See config/config.exs. + """ + + # If you want to customize a particular status code, + # you may add your own clauses, such as: + # + # def render("500.json", _assigns) do + # %{errors: %{detail: "Internal Server Error"}} + # end + + # By default, Phoenix returns the status message from + # the template name. For example, "404.json" becomes + # "Not Found". + def render(template, _assigns) do + %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} + end +end diff --git a/telemetry_api/lib/telemetry_api_web/controllers/fallback_controller.ex b/telemetry_api/lib/telemetry_api_web/controllers/fallback_controller.ex new file mode 100644 index 000000000..2b396181f --- /dev/null +++ b/telemetry_api/lib/telemetry_api_web/controllers/fallback_controller.ex @@ -0,0 +1,24 @@ +defmodule TelemetryApiWeb.FallbackController do + @moduledoc """ + Translates controller action results into valid `Plug.Conn` responses. + + See `Phoenix.Controller.action_fallback/1` for more details. + """ + use TelemetryApiWeb, :controller + + # This clause handles errors returned by Ecto's insert/update/delete. + def call(conn, {:error, %Ecto.Changeset{} = changeset}) do + conn + |> put_status(:unprocessable_entity) + |> put_view(json: TelemetryApiWeb.ChangesetJSON) + |> render(:error, changeset: changeset) + end + + # This clause is an example of how to handle resources that cannot be found. + def call(conn, {:error, :not_found}) do + conn + |> put_status(:not_found) + |> put_view(html: TelemetryApiWeb.ErrorHTML, json: TelemetryApiWeb.ErrorJSON) + |> render(:"404") + end +end diff --git a/telemetry_api/lib/telemetry_api_web/controllers/operator_controller.ex b/telemetry_api/lib/telemetry_api_web/controllers/operator_controller.ex new file mode 100644 index 000000000..cefb88ca4 --- /dev/null +++ b/telemetry_api/lib/telemetry_api_web/controllers/operator_controller.ex @@ -0,0 +1,43 @@ +defmodule TelemetryApiWeb.OperatorController do + use TelemetryApiWeb, :controller + + alias TelemetryApi.Operators + alias TelemetryApi.Operators.Operator + + action_fallback TelemetryApiWeb.FallbackController + + def index(conn, _params) do + operators = Operators.list_operators() + render(conn, :index, operators: operators) + end + + def create(conn, operator_params) do + with {:ok, %Operator{} = operator} <- Operators.create_operator(operator_params) do + conn + |> put_status(:created) + |> put_resp_header("location", ~p"/api/operators/#{operator}") + |> render(:show, operator: operator) + end + end + + def show(conn, %{"id" => id}) do + operator = Operators.get_operator!(id) + render(conn, :show, operator: operator) + end + + # def update(conn, %{"id" => id, "operator" => operator_params}) do + # operator = Operators.get_operator!(id) + + # with {:ok, %Operator{} = operator} <- Operators.update_operator(operator, operator_params) do + # render(conn, :show, operator: operator) + # end + # end + + # def delete(conn, %{"id" => id}) do + # operator = Operators.get_operator!(id) + + # with {:ok, %Operator{}} <- Operators.delete_operator(operator) do + # send_resp(conn, :no_content, "") + # end + # end +end diff --git a/telemetry_api/lib/telemetry_api_web/controllers/operator_json.ex b/telemetry_api/lib/telemetry_api_web/controllers/operator_json.ex new file mode 100644 index 000000000..5d0a27951 --- /dev/null +++ b/telemetry_api/lib/telemetry_api_web/controllers/operator_json.ex @@ -0,0 +1,25 @@ +defmodule TelemetryApiWeb.OperatorJSON do + alias TelemetryApi.Operators.Operator + + @doc """ + Renders a list of operators. + """ + def index(%{operators: operators}) do + for(operator <- operators, do: data(operator)) + end + + @doc """ + Renders a single operator. + """ + def show(%{operator: operator}) do + data(operator) + end + + defp data(%Operator{} = operator) do + %{ + id: operator.id, + address: operator.address, + version: operator.version + } + end +end diff --git a/telemetry_api/lib/telemetry_api_web/endpoint.ex b/telemetry_api/lib/telemetry_api_web/endpoint.ex new file mode 100644 index 000000000..861505826 --- /dev/null +++ b/telemetry_api/lib/telemetry_api_web/endpoint.ex @@ -0,0 +1,51 @@ +defmodule TelemetryApiWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :telemetry_api + + # The session will be stored in the cookie and signed, + # this means its contents can be read but not tampered with. + # Set :encryption_salt if you would also like to encrypt it. + @session_options [ + store: :cookie, + key: "_telemetry_api_key", + signing_salt: "3K5HyxYk", + same_site: "Lax" + ] + + socket "/live", Phoenix.LiveView.Socket, + websocket: [connect_info: [session: @session_options]], + longpoll: [connect_info: [session: @session_options]] + + # Serve at "/" the static files from "priv/static" directory. + # + # You should set gzip to true if you are running phx.digest + # when deploying your static files in production. + plug Plug.Static, + at: "/", + from: :telemetry_api, + gzip: false, + only: TelemetryApiWeb.static_paths() + + # Code reloading can be explicitly enabled under the + # :code_reloader configuration of your endpoint. + if code_reloading? do + plug Phoenix.CodeReloader + plug Phoenix.Ecto.CheckRepoStatus, otp_app: :telemetry_api + end + + plug Phoenix.LiveDashboard.RequestLogger, + param_key: "request_logger", + cookie_key: "request_logger" + + plug Plug.RequestId + plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] + + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Phoenix.json_library() + + plug Plug.MethodOverride + plug Plug.Head + plug Plug.Session, @session_options + plug TelemetryApiWeb.Router +end diff --git a/telemetry_api/lib/telemetry_api_web/router.ex b/telemetry_api/lib/telemetry_api_web/router.ex new file mode 100644 index 000000000..217982b11 --- /dev/null +++ b/telemetry_api/lib/telemetry_api_web/router.ex @@ -0,0 +1,28 @@ +defmodule TelemetryApiWeb.Router do + use TelemetryApiWeb, :router + + pipeline :api do + plug :accepts, ["json"] + end + + scope "/api", TelemetryApiWeb do + pipe_through :api + resources "/operators", OperatorController, only: [:index, :show, :create] + end + + # Enable LiveDashboard in development + if Application.compile_env(:telemetry_api, :dev_routes) do + # If you want to use the LiveDashboard in production, you should put + # it behind authentication and allow only admins to access it. + # If your application does not have an admins-only section yet, + # you can use Plug.BasicAuth to set up some basic authentication + # as long as you are also using SSL (which you should anyway). + import Phoenix.LiveDashboard.Router + + scope "/dev" do + pipe_through [:fetch_session, :protect_from_forgery] + + live_dashboard "/dashboard", metrics: TelemetryApiWeb.Telemetry + end + end +end diff --git a/telemetry_api/lib/telemetry_api_web/telemetry.ex b/telemetry_api/lib/telemetry_api_web/telemetry.ex new file mode 100644 index 000000000..b19a5a70c --- /dev/null +++ b/telemetry_api/lib/telemetry_api_web/telemetry.ex @@ -0,0 +1,92 @@ +defmodule TelemetryApiWeb.Telemetry do + use Supervisor + import Telemetry.Metrics + + def start_link(arg) do + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + end + + @impl true + def init(_arg) do + children = [ + # Telemetry poller will execute the given period measurements + # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} + # Add reporters as children of your supervision tree. + # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + + def metrics do + [ + # Phoenix Metrics + summary("phoenix.endpoint.start.system_time", + unit: {:native, :millisecond} + ), + summary("phoenix.endpoint.stop.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.start.system_time", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.exception.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.stop.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + summary("phoenix.socket_connected.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.channel_joined.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.channel_handled_in.duration", + tags: [:event], + unit: {:native, :millisecond} + ), + + # Database Metrics + summary("telemetry_api.repo.query.total_time", + unit: {:native, :millisecond}, + description: "The sum of the other measurements" + ), + summary("telemetry_api.repo.query.decode_time", + unit: {:native, :millisecond}, + description: "The time spent decoding the data received from the database" + ), + summary("telemetry_api.repo.query.query_time", + unit: {:native, :millisecond}, + description: "The time spent executing the query" + ), + summary("telemetry_api.repo.query.queue_time", + unit: {:native, :millisecond}, + description: "The time spent waiting for a database connection" + ), + summary("telemetry_api.repo.query.idle_time", + unit: {:native, :millisecond}, + description: + "The time the connection spent waiting before being checked out for the query" + ), + + # VM Metrics + summary("vm.memory.total", unit: {:byte, :kilobyte}), + summary("vm.total_run_queue_lengths.total"), + summary("vm.total_run_queue_lengths.cpu"), + summary("vm.total_run_queue_lengths.io") + ] + end + + defp periodic_measurements do + [ + # A module, function and arguments to be invoked periodically. + # This function must call :telemetry.execute/3 and a metric must be added above. + # {TelemetryApiWeb, :count_users, []} + ] + end +end diff --git a/telemetry_api/mix.exs b/telemetry_api/mix.exs new file mode 100644 index 000000000..c78f7c5f2 --- /dev/null +++ b/telemetry_api/mix.exs @@ -0,0 +1,62 @@ +defmodule TelemetryApi.MixProject do + use Mix.Project + + def project do + [ + app: :telemetry_api, + version: "0.1.0", + elixir: "~> 1.14", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + aliases: aliases(), + deps: deps() + ] + end + + # Configuration for the OTP application. + # + # Type `mix help compile.app` for more information. + def application do + [ + mod: {TelemetryApi.Application, []}, + extra_applications: [:logger, :runtime_tools] + ] + end + + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + # Specifies your project dependencies. + # + # Type `mix help deps` for examples and options. + defp deps do + [ + {:phoenix, "~> 1.7.14"}, + {:phoenix_ecto, "~> 4.5"}, + {:ecto_sql, "~> 3.10"}, + {:postgrex, ">= 0.0.0"}, + {:phoenix_live_dashboard, "~> 0.8.3"}, + {:telemetry_metrics, "~> 1.0"}, + {:telemetry_poller, "~> 1.0"}, + {:jason, "~> 1.2"}, + {:dns_cluster, "~> 0.1.1"}, + {:bandit, "~> 1.5"} + ] + end + + # Aliases are shortcuts or tasks specific to the current project. + # For example, to install project dependencies and perform other setup tasks, run: + # + # $ mix setup + # + # See the documentation for `Mix` for more info on aliases. + defp aliases do + [ + setup: ["deps.get", "ecto.setup"], + "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], + "ecto.reset": ["ecto.drop", "ecto.setup"], + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] + ] + end +end diff --git a/telemetry_api/mix.lock b/telemetry_api/mix.lock new file mode 100644 index 000000000..748eb9357 --- /dev/null +++ b/telemetry_api/mix.lock @@ -0,0 +1,28 @@ +%{ + "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, + "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, + "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"}, + "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"}, + "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.17", "f396bbdaf4ba227b82251eb75ac0afa6b3da5e509bc0d030206374237dfc9450", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61d741ffb78c85fdbca0de084da6a48f8ceb5261a79165b5a0b59e5f65ce98b"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, + "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, +} diff --git a/telemetry_api/priv/repo/migrations/.formatter.exs b/telemetry_api/priv/repo/migrations/.formatter.exs new file mode 100644 index 000000000..49f9151ed --- /dev/null +++ b/telemetry_api/priv/repo/migrations/.formatter.exs @@ -0,0 +1,4 @@ +[ + import_deps: [:ecto_sql], + inputs: ["*.exs"] +] diff --git a/telemetry_api/priv/repo/migrations/20240917212329_create_operators.exs b/telemetry_api/priv/repo/migrations/20240917212329_create_operators.exs new file mode 100644 index 000000000..3c1e5bbb1 --- /dev/null +++ b/telemetry_api/priv/repo/migrations/20240917212329_create_operators.exs @@ -0,0 +1,12 @@ +defmodule TelemetryApi.Repo.Migrations.CreateOperators do + use Ecto.Migration + + def change do + create table(:operators) do + add :address, :string + add :version, :string + + timestamps(type: :utc_datetime) + end + end +end diff --git a/telemetry_api/priv/repo/seeds.exs b/telemetry_api/priv/repo/seeds.exs new file mode 100644 index 000000000..7b50b07e1 --- /dev/null +++ b/telemetry_api/priv/repo/seeds.exs @@ -0,0 +1,11 @@ +# Script for populating the database. You can run it as: +# +# mix run priv/repo/seeds.exs +# +# Inside the script, you can read and write to any of your +# repositories directly: +# +# TelemetryApi.Repo.insert!(%TelemetryApi.SomeSchema{}) +# +# We recommend using the bang functions (`insert!`, `update!` +# and so on) as they will fail if something goes wrong. diff --git a/telemetry_api/priv/static/favicon.ico b/telemetry_api/priv/static/favicon.ico new file mode 100644 index 000000000..7f372bfc2 Binary files /dev/null and b/telemetry_api/priv/static/favicon.ico differ diff --git a/telemetry_api/priv/static/robots.txt b/telemetry_api/priv/static/robots.txt new file mode 100644 index 000000000..26e06b5f1 --- /dev/null +++ b/telemetry_api/priv/static/robots.txt @@ -0,0 +1,5 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/telemetry_api/test/support/conn_case.ex b/telemetry_api/test/support/conn_case.ex new file mode 100644 index 000000000..b51e7f0e0 --- /dev/null +++ b/telemetry_api/test/support/conn_case.ex @@ -0,0 +1,38 @@ +defmodule TelemetryApiWeb.ConnCase do + @moduledoc """ + This module defines the test case to be used by + tests that require setting up a connection. + + Such tests rely on `Phoenix.ConnTest` and also + import other functionality to make it easier + to build common data structures and query the data layer. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use TelemetryApiWeb.ConnCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # The default endpoint for testing + @endpoint TelemetryApiWeb.Endpoint + + use TelemetryApiWeb, :verified_routes + + # Import conveniences for testing with connections + import Plug.Conn + import Phoenix.ConnTest + import TelemetryApiWeb.ConnCase + end + end + + setup tags do + TelemetryApi.DataCase.setup_sandbox(tags) + {:ok, conn: Phoenix.ConnTest.build_conn()} + end +end diff --git a/telemetry_api/test/support/data_case.ex b/telemetry_api/test/support/data_case.ex new file mode 100644 index 000000000..7a30b5a25 --- /dev/null +++ b/telemetry_api/test/support/data_case.ex @@ -0,0 +1,58 @@ +defmodule TelemetryApi.DataCase do + @moduledoc """ + This module defines the setup for tests requiring + access to the application's data layer. + + You may define functions here to be used as helpers in + your tests. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use TelemetryApi.DataCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + alias TelemetryApi.Repo + + import Ecto + import Ecto.Changeset + import Ecto.Query + import TelemetryApi.DataCase + end + end + + setup tags do + TelemetryApi.DataCase.setup_sandbox(tags) + :ok + end + + @doc """ + Sets up the sandbox based on the test tags. + """ + def setup_sandbox(tags) do + pid = Ecto.Adapters.SQL.Sandbox.start_owner!(TelemetryApi.Repo, shared: not tags[:async]) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) + end + + @doc """ + A helper that transforms changeset errors into a map of messages. + + assert {:error, changeset} = Accounts.create_user(%{password: "short"}) + assert "password is too short" in errors_on(changeset).password + assert %{password: ["password is too short"]} = errors_on(changeset) + + """ + def errors_on(changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> + Regex.replace(~r"%{(\w+)}", message, fn _, key -> + opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() + end) + end) + end +end diff --git a/telemetry_api/test/support/fixtures/urls_fixtures.ex b/telemetry_api/test/support/fixtures/urls_fixtures.ex new file mode 100644 index 000000000..f94b63845 --- /dev/null +++ b/telemetry_api/test/support/fixtures/urls_fixtures.ex @@ -0,0 +1,21 @@ +defmodule TelemetryApi.UrlsFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `TelemetryApi.Urls` context. + """ + + @doc """ + Generate a operator. + """ + def operator_fixture(attrs \\ %{}) do + {:ok, operator} = + attrs + |> Enum.into(%{ + address: "some address", + version: "some version" + }) + |> TelemetryApi.Urls.create_operator() + + operator + end +end diff --git a/telemetry_api/test/telemetry_api/urls_test.exs b/telemetry_api/test/telemetry_api/urls_test.exs new file mode 100644 index 000000000..618f10f85 --- /dev/null +++ b/telemetry_api/test/telemetry_api/urls_test.exs @@ -0,0 +1,61 @@ +defmodule TelemetryApi.UrlsTest do + use TelemetryApi.DataCase + + alias TelemetryApi.Urls + + describe "operators" do + alias TelemetryApi.Urls.Operator + + import TelemetryApi.UrlsFixtures + + @invalid_attrs %{version: nil, address: nil} + + test "list_operators/0 returns all operators" do + operator = operator_fixture() + assert Urls.list_operators() == [operator] + end + + test "get_operator!/1 returns the operator with given id" do + operator = operator_fixture() + assert Urls.get_operator!(operator.id) == operator + end + + test "create_operator/1 with valid data creates a operator" do + valid_attrs = %{version: "some version", address: "some address"} + + assert {:ok, %Operator{} = operator} = Urls.create_operator(valid_attrs) + assert operator.version == "some version" + assert operator.address == "some address" + end + + test "create_operator/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Urls.create_operator(@invalid_attrs) + end + + test "update_operator/2 with valid data updates the operator" do + operator = operator_fixture() + update_attrs = %{version: "some updated version", address: "some updated address"} + + assert {:ok, %Operator{} = operator} = Urls.update_operator(operator, update_attrs) + assert operator.version == "some updated version" + assert operator.address == "some updated address" + end + + test "update_operator/2 with invalid data returns error changeset" do + operator = operator_fixture() + assert {:error, %Ecto.Changeset{}} = Urls.update_operator(operator, @invalid_attrs) + assert operator == Urls.get_operator!(operator.id) + end + + test "delete_operator/1 deletes the operator" do + operator = operator_fixture() + assert {:ok, %Operator{}} = Urls.delete_operator(operator) + assert_raise Ecto.NoResultsError, fn -> Urls.get_operator!(operator.id) end + end + + test "change_operator/1 returns a operator changeset" do + operator = operator_fixture() + assert %Ecto.Changeset{} = Urls.change_operator(operator) + end + end +end diff --git a/telemetry_api/test/telemetry_api_web/controllers/error_json_test.exs b/telemetry_api/test/telemetry_api_web/controllers/error_json_test.exs new file mode 100644 index 000000000..5cb349fdc --- /dev/null +++ b/telemetry_api/test/telemetry_api_web/controllers/error_json_test.exs @@ -0,0 +1,12 @@ +defmodule TelemetryApiWeb.ErrorJSONTest do + use TelemetryApiWeb.ConnCase, async: true + + test "renders 404" do + assert TelemetryApiWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} + end + + test "renders 500" do + assert TelemetryApiWeb.ErrorJSON.render("500.json", %{}) == + %{errors: %{detail: "Internal Server Error"}} + end +end diff --git a/telemetry_api/test/telemetry_api_web/controllers/operator_controller_test.exs b/telemetry_api/test/telemetry_api_web/controllers/operator_controller_test.exs new file mode 100644 index 000000000..bcb8b8082 --- /dev/null +++ b/telemetry_api/test/telemetry_api_web/controllers/operator_controller_test.exs @@ -0,0 +1,88 @@ +defmodule TelemetryApiWeb.OperatorControllerTest do + use TelemetryApiWeb.ConnCase + + import TelemetryApi.UrlsFixtures + + alias TelemetryApi.Urls.Operator + + @create_attrs %{ + version: "some version", + address: "some address" + } + @update_attrs %{ + version: "some updated version", + address: "some updated address" + } + @invalid_attrs %{version: nil, address: nil} + + setup %{conn: conn} do + {:ok, conn: put_req_header(conn, "accept", "application/json")} + end + + describe "index" do + test "lists all operators", %{conn: conn} do + conn = get(conn, ~p"/api/operators") + assert json_response(conn, 200)["data"] == [] + end + end + + describe "create operator" do + test "renders operator when data is valid", %{conn: conn} do + conn = post(conn, ~p"/api/operators", operator: @create_attrs) + assert %{"id" => id} = json_response(conn, 201)["data"] + + conn = get(conn, ~p"/api/operators/#{id}") + + assert %{ + "id" => ^id, + "address" => "some address", + "version" => "some version" + } = json_response(conn, 200)["data"] + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = post(conn, ~p"/api/operators", operator: @invalid_attrs) + assert json_response(conn, 422)["errors"] != %{} + end + end + + describe "update operator" do + setup [:create_operator] + + test "renders operator when data is valid", %{conn: conn, operator: %Operator{id: id} = operator} do + conn = put(conn, ~p"/api/operators/#{operator}", operator: @update_attrs) + assert %{"id" => ^id} = json_response(conn, 200)["data"] + + conn = get(conn, ~p"/api/operators/#{id}") + + assert %{ + "id" => ^id, + "address" => "some updated address", + "version" => "some updated version" + } = json_response(conn, 200)["data"] + end + + test "renders errors when data is invalid", %{conn: conn, operator: operator} do + conn = put(conn, ~p"/api/operators/#{operator}", operator: @invalid_attrs) + assert json_response(conn, 422)["errors"] != %{} + end + end + + describe "delete operator" do + setup [:create_operator] + + test "deletes chosen operator", %{conn: conn, operator: operator} do + conn = delete(conn, ~p"/api/operators/#{operator}") + assert response(conn, 204) + + assert_error_sent 404, fn -> + get(conn, ~p"/api/operators/#{operator}") + end + end + end + + defp create_operator(_) do + operator = operator_fixture() + %{operator: operator} + end +end diff --git a/telemetry_api/test/test_helper.exs b/telemetry_api/test/test_helper.exs new file mode 100644 index 000000000..a354f9030 --- /dev/null +++ b/telemetry_api/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +Ecto.Adapters.SQL.Sandbox.mode(TelemetryApi.Repo, :manual)