From 6e36a9c3fb482cc993a3fa6511c51cc19b59d5a2 Mon Sep 17 00:00:00 2001 From: kballou Date: Tue, 27 Dec 2016 10:02:42 -0700 Subject: Add base datadog client This adds a base HTTPoison Datadog Client library. --- .travis.yml | 13 +++++ config/.credo.exs | 65 ++++++++++++++++++++++ config/config.exs | 7 +++ config/dev.exs | 1 + config/docs.exs | 1 + config/prod.exs | 1 + config/test.exs | 5 ++ lib/exdatadog.ex | 123 +++++++++++++++++++++++++++++++++++++++++ lib/exdatadog/client.ex | 33 +++++++++++ lib/exdatadog/config.ex | 15 +++++ mix.exs | 45 +++++++++++++++ mix.lock | 18 ++++++ test/exdatadog/client_test.exs | 39 +++++++++++++ test/exdatadog/config_test.exs | 30 ++++++++++ test/exdatadog_test.exs | 64 +++++++++++++++++++++ test/test_helper.exs | 1 + 16 files changed, 461 insertions(+) create mode 100644 .travis.yml create mode 100644 config/.credo.exs create mode 100644 config/config.exs create mode 100644 config/dev.exs create mode 100644 config/docs.exs create mode 100644 config/prod.exs create mode 100644 config/test.exs create mode 100644 lib/exdatadog.ex create mode 100644 lib/exdatadog/client.ex create mode 100644 lib/exdatadog/config.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 test/exdatadog/client_test.exs create mode 100644 test/exdatadog/config_test.exs create mode 100644 test/exdatadog_test.exs create mode 100644 test/test_helper.exs diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..914743f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +sudo: false +language: elixir +elixir: + - 1.3.2 + - 1.3.3 + - 1.3.4 +otp_release: + - 18.3 + - 19.0 + - 19.1 + - 19.2 +script: + - mix test --cover diff --git a/config/.credo.exs b/config/.credo.exs new file mode 100644 index 0000000..3440473 --- /dev/null +++ b/config/.credo.exs @@ -0,0 +1,65 @@ +%{ + configs: [ + %{ + name: "default", + files: %{ + included: ["lib/", "test/"], + excluded: [~r"/_build/", ~r"/deps/"] + }, + requires: [], + check_for_updates: true, + checks: [ + {Credo.Check.Consistency.ExceptionNames}, + {Credo.Check.Consistency.LineEndings}, + {Credo.Check.Consistency.SpaceAroundOperators}, + {Credo.Check.Consistency.SpaceInParentheses}, + {Credo.Check.Consistency.TabsOrSpaces}, + + {Credo.Check.Design.AliasUsage, priority: :low}, + + {Credo.Check.Design.DuplicatedCode, excluded_macros: []}, + + {Credo.Check.Design.TagTODO, exit_status: 2}, + {Credo.Check.Design.TagFIXME}, + + {Credo.Check.Readability.FunctionNames}, + {Credo.Check.Readability.LargeNumbers}, + {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80}, + {Credo.Check.Readability.ModuleAttributeNames}, + {Credo.Check.Readability.ModuleDoc}, + {Credo.Check.Readability.ModuleNames}, + {Credo.Check.Readability.ParenthesesInCondition}, + {Credo.Check.Readability.PredicateFunctionNames}, + {Credo.Check.Readability.TrailingBlankLine}, + {Credo.Check.Readability.TrailingWhiteSpace}, + {Credo.Check.Readability.VariableNames}, + + {Credo.Check.Refactor.ABCSize}, + {Credo.Check.Refactor.CondStatements}, + {Credo.Check.Refactor.FunctionArity}, + {Credo.Check.Refactor.MatchInCondition}, + {Credo.Check.Refactor.PipeChainStart}, + {Credo.Check.Refactor.CyclomaticComplexity}, + {Credo.Check.Refactor.NegatedConditionsInUnless}, + {Credo.Check.Refactor.NegatedConditionsWithElse}, + {Credo.Check.Refactor.Nesting}, + {Credo.Check.Refactor.UnlessWithElse}, + + {Credo.Check.Warning.IExPry}, + {Credo.Check.Warning.IoInspect}, + {Credo.Check.Warning.NameRedeclarationByAssignment}, + {Credo.Check.Warning.NameRedeclarationByCase}, + {Credo.Check.Warning.NameRedeclarationByDef}, + {Credo.Check.Warning.NameRedeclarationByFn}, + {Credo.Check.Warning.OperationOnSameValues}, + {Credo.Check.Warning.BoolOperationOnSameValues}, + {Credo.Check.Warning.UnusedEnumOperation}, + {Credo.Check.Warning.UnusedKeywordOperation}, + {Credo.Check.Warning.UnusedListOperation}, + {Credo.Check.Warning.UnusedStringOperation}, + {Credo.Check.Warning.UnusedTupleOperation}, + {Credo.Check.Warning.OperationWithConstantResult}, + ] + } + ] +} diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..2d6e3c7 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,7 @@ +use Mix.Config + +config :exdatadog, + api_key: {:system, "DATADOG_API_KEY"}, + app_key: {:system, "DATADOG_APP_KEY"} + +import_config "#{Mix.env}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..d2d855e --- /dev/null +++ b/config/dev.exs @@ -0,0 +1 @@ +use Mix.Config diff --git a/config/docs.exs b/config/docs.exs new file mode 100644 index 0000000..d2d855e --- /dev/null +++ b/config/docs.exs @@ -0,0 +1 @@ +use Mix.Config diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..d2d855e --- /dev/null +++ b/config/prod.exs @@ -0,0 +1 @@ +use Mix.Config diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..e9f574d --- /dev/null +++ b/config/test.exs @@ -0,0 +1,5 @@ +use Mix.Config + +config :exdatadog, + api_key: "1234", + app_key: "abcd" diff --git a/lib/exdatadog.ex b/lib/exdatadog.ex new file mode 100644 index 0000000..cb1c559 --- /dev/null +++ b/lib/exdatadog.ex @@ -0,0 +1,123 @@ +defmodule Exdatadog do + @moduledoc """ + HTTPoison Client for exdatadog + """ + use HTTPoison.Base + + alias Exdatadog.Client + alias HTTPoison.Response + + @user_agent [{"user-agent", "exdatadog"}] + + @type response :: {integer, any} | map + + @spec process_response_body(binary) :: map + def process_response_body(""), do: nil + def process_response_body(body), do: Poison.decode!(body) + + @spec process_response(HTTPoison.Response.t) :: response + def process_response(%Response{status_code: status_code, body: body}) do + {status_code, body} + end + + def post(path, client, body) do + _request(:post, url(client, path), client.auth, body) + end + + def get(path, client, params \\ [], _ \\ []) do + url = + client + |> url(path) + |> add_params_to_url(params) + + _request(:get, url, client.auth) + + end + + def _request(method, url, auth, body \\ "") do + json_request(method, + add_params_to_url(url, auth_params(auth)), + body, + @user_agent) + end + + def json_request(method, url, body \\ "", headers \\ [], options \\ []) do + raw_request(method, url, Poison.encode!(body), headers, options) + end + + def raw_request(method, url, body \\ "", headers \\ [], options \\ []) do + method + |> request!(url, body, headers, options) + |> process_response + end + + @spec url(Exdatadog.Client.t, binary) :: binary + defp url(_client = %Client{endpoint: endpoint}, path) do + endpoint <> path + end + + @doc """ + Take an existing URI and add addition parameters, merging as necessary + + ## Examples + iex> add_params_to_url("http://example.com/wat", []) + "http://example.com/wat" + iex> add_params_to_url("http://example.com/wat", [q: 1]) + "http://example.com/wat?q=1" + iex> add_params_to_url("http://example.com/wat", [q: 1, t: 2]) + "http://example.com/wat?q=1&t=2" + iex> add_params_to_url("http://example.com/wat", %{q: 1, t: 2}) + "http://example.com/wat?q=1&t=2" + iex> add_params_to_url("http://example.com/wat?q=1&t=2", []) + "http://example.com/wat?q=1&t=2" + iex> add_params_to_url("http://example.com/wat?q=1", [t: 2]) + "http://example.com/wat?q=1&t=2" + iex> add_params_to_url("http://example.com/wat?q=1", [q: 3, t: 2]) + "http://example.com/wat?q=3&t=2" + iex> add_params_to_url("http://example.com/wat?q=1&s=4", [q: 3, t: 2]) + "http://example.com/wat?q=3&s=4&t=2" + iex> add_params_to_url("http://example.com/wat?q=1&s=4", %{q: 3, t: 2}) + "http://example.com/wat?q=3&s=4&t=2" + + """ + @spec add_params_to_url(binary, list) :: binary + def add_params_to_url(url, params) do + url + |> URI.parse + |> merge_uri_params(params) + |> to_string + end + + defp merge_uri_params(uri, []), do: uri + defp merge_uri_params(%URI{query: nil} = uri, params) + when is_list(params) or is_map(params) do + uri + |> Map.put(:query, URI.encode_query(params)) + end + defp merge_uri_params(%URI{} = uri, params) + when is_list(params) or is_map(params) do + uri + |> Map.update!(:query, fn q -> + q + |> URI.decode_query + |> Map.merge(param_list_to_map_with_string_keys(params)) + |> URI.encode_query + end) + end + + defp param_list_to_map_with_string_keys(list) + when is_list(list) or is_map(list) do + for {key, value} <- list, into: Map.new do + {"#{key}", value} + end + end + + def auth_params(%{api_key: api_key, app_key: app_key}) do + [application_key: app_key, api_key: api_key] + end + def auth_params(%{api_key: api_key}) do + [api_key: api_key] + end + def auth_params(_), do: [] + +end diff --git a/lib/exdatadog/client.ex b/lib/exdatadog/client.ex new file mode 100644 index 0000000..02c1017 --- /dev/null +++ b/lib/exdatadog/client.ex @@ -0,0 +1,33 @@ +defmodule Exdatadog.Client do + @moduledoc """ + Datadog Client record for endpoint and authentication data + """ + + import Exdatadog.Config, only: [get_env_var: 2] + + defstruct auth: nil, endpoint: "https://app.datadoghq.com/" + + @type auth :: %{api_key: binary, app_key: binary} | %{api_key: binary} + @type t :: %__MODULE__{auth: auth, endpoint: binary} + + @spec new() :: t + def new() do + auth = %{api_key: get_env_var(:exdatadog, :api_key), + app_key: get_env_var(:exdatadog, :app_key)} + %__MODULE__{auth: auth} + end + + @spec new(auth) :: t + def new(auth), do: %__MODULE__{auth: auth} + + @spec new(auth, binary) :: t + def new(auth, endpoint) do + endpoint = if String.ends_with?(endpoint, "/") do + endpoint + else + endpoint <> "/" + end + %__MODULE__{auth: auth, endpoint: endpoint} + end + +end diff --git a/lib/exdatadog/config.ex b/lib/exdatadog/config.ex new file mode 100644 index 0000000..f686309 --- /dev/null +++ b/lib/exdatadog/config.ex @@ -0,0 +1,15 @@ +defmodule Exdatadog.Config do + @moduledoc """ + Provides helper functions for interacting with Application variables + """ + + def get_env_var(app, key, default \\ nil) do + app + |> Application.get_env(key, default) + |> case do + {:system, env_key} -> System.get_env(env_key) + env_var -> env_var + end + end + +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..d481956 --- /dev/null +++ b/mix.exs @@ -0,0 +1,45 @@ +defmodule Exdatadog.Mixfile do + use Mix.Project + + def project do + [app: :exdatadog, + description: "Elixir Datadog API Client", + package: package(), + version: "0.1.0", + elixir: "~> 1.3", + build_embedded: Mix.env == :prod, + start_permanent: Mix.env == :prod, + deps: deps(), + test_coverage: [tool: ExCoveralls], + docs: docs()] + end + + def application do + [applications: [:logger, :httpoison]] + end + + defp deps do + [{:credo, "~> 0.5.0", only: :dev}, + {:excoveralls, "~> 0.5", only: :test}, + {:earmark, "~> 0.1", only: :docs}, + {:ex_doc, "~> 0.11", only: :docs}, + {:exvcr, "~> 0.8.4", only: :test}, + {:httpoison, "~> 0.10.0"}, + {:meck, "~> 0.8", only: :test}, + {:poison, "~> 2.2.0"}] + end + + defp docs do + [extras: ["README.md"]] + end + + defp package do + [maintainers: ["Kenny Ballou"], + licenses: ["Apache 2.0"], + links: %{"Git" => "https://git.devnulllabs.io/exdatadog.git", + "GitHub" => "https://github.com/kennyballou/exdatadog.git", + "Hex" => "https://hex.pm/packages/exdatadog", + "Hex Docs" => "https://hexdocs.pm/exdatadog"}, + files: ~w(mix.exs README.md LICENSE lib)] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..eecdfcc --- /dev/null +++ b/mix.lock @@ -0,0 +1,18 @@ +%{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, + "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []}, + "credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, + "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []}, + "ex_doc": {:hex, :ex_doc, "0.12.0", "b774aabfede4af31c0301aece12371cbd25995a21bb3d71d66f5c2fe074c603f", [:mix], [{:earmark, "~> 0.2", [hex: :earmark, optional: false]}]}, + "exactor": {:hex, :exactor, "2.2.2", "90b27d72c05614801a60f8400afd4e4346dfc33ea9beffe3b98a794891d2ff96", [:mix], []}, + "excoveralls": {:hex, :excoveralls, "0.5.7", "5d26e4a7cdf08294217594a1b0643636accc2ad30e984d62f1d166f70629ff50", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, + "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, + "exvcr": {:hex, :exvcr, "0.8.4", "23ce3a7ff428ed98127292da1b6d04418064391d1045c00cd8ee2da37c5b839b", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, optional: false]}, {:exjsx, "~> 3.2", [hex: :exjsx, optional: false]}, {:httpoison, "~> 0.8", [hex: :httpoison, optional: true]}, {:httpotion, "~> 3.0", [hex: :httpotion, optional: true]}, {:ibrowse, "~> 4.2.2", [hex: :ibrowse, optional: true]}, {:meck, "~> 0.8.3", [hex: :meck, optional: false]}]}, + "hackney": {:hex, :hackney, "1.6.5", "8c025ee397ac94a184b0743c73b33b96465e85f90a02e210e86df6cbafaa5065", [:rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, + "httpoison": {:hex, :httpoison, "0.10.0", "4727b3a5e57e9a4ff168a3c2883e20f1208103a41bccc4754f15a9366f49b676", [:mix], [{:hackney, "~> 1.6.3", [hex: :hackney, optional: false]}]}, + "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, + "jsx": {:hex, :jsx, "2.8.1", "1453b4eb3615acb3e2cd0a105d27e6761e2ed2e501ac0b390f5bbec497669846", [:mix, :rebar3], []}, + "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:make, :rebar], []}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, + "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}} diff --git a/test/exdatadog/client_test.exs b/test/exdatadog/client_test.exs new file mode 100644 index 0000000..dc63c1b --- /dev/null +++ b/test/exdatadog/client_test.exs @@ -0,0 +1,39 @@ +defmodule Exdatadog.Client.Test do + @moduledoc """ + Provides Tests for the Exdatadog.Client module + """ + use ExUnit.Case, async: false + + @endpoint "https://app.datadoghq.com/" + + alias Exdatadog.Client + + test "can create new client" do + expected = %Client{endpoint: @endpoint, auth: %{api_key: "1234", + app_key: "abcd"}} + assert Client.new() == expected + end + + test "can create new client with api auth" do + actual = Client.new(%{api_key: "1234"}) + assert actual == %Client{endpoint: @endpoint, auth: %{api_key: "1234"}} + end + + test "can create new client with api and app auth" do + actual = Client.new(%{api_key: "1234", app_key: "abcd"}) + assert actual == %Client{endpoint: @endpoint, auth: %{api_key: "1234", + app_key: "abcd"}} + end + + test "can create new client with auth and custom endpoint" do + actual = Client.new(%{api_key: "1234"}, "https://test.datadoghq.com/") + assert actual == %Client{endpoint: "https://test.datadoghq.com/", + auth: %{api_key: "1234"}} + end + + test "trailing / appended to endpoint without" do + actual = Client.new(nil, "https://test.datadoghq.com") + assert actual == %Client{endpoint: "https://test.datadoghq.com/", auth: nil} + end + +end diff --git a/test/exdatadog/config_test.exs b/test/exdatadog/config_test.exs new file mode 100644 index 0000000..2be0c38 --- /dev/null +++ b/test/exdatadog/config_test.exs @@ -0,0 +1,30 @@ +defmodule Exdatadog.Config.Test do + @moduledoc """ + Provides tests for Exdatadog.Config + """ + use ExUnit.Case + + import Exdatadog.Config + + setup_all do + System.put_env("TEST_VAR", "BAR") + Application.put_env(:test_app, :test_key, {:system, "TEST_VAR"}) + Application.put_env(:test_app, :test_foo, "FOO") + + on_exit fn -> + System.delete_env("TEST_VAR") + Application.delete_env(:test_app, :test_key) + Application.delete_env(:test_app, :test_foo) + end + + end + + test "can read variable from application settings" do + assert get_env_var(:test_app, :test_foo) == "FOO" + end + + test "can read environment variables for settings" do + assert get_env_var(:test_app, :test_key) == "BAR" + end + +end diff --git a/test/exdatadog_test.exs b/test/exdatadog_test.exs new file mode 100644 index 0000000..5e752ab --- /dev/null +++ b/test/exdatadog_test.exs @@ -0,0 +1,64 @@ +defmodule ExdatadogTest do + @moduledoc """ + Provides tests for the Exdatadog module + """ + use ExUnit.Case + + alias HTTPoison.Response + import Exdatadog + + doctest Exdatadog + + setup_all do + :meck.new(Poison, [:no_link]) + + on_exit fn -> + :meck.unload(Poison) + end + end + + test "auth_params using api_key" do + assert auth_params(%{api_key: "1234"}) == [api_key: "1234"] + end + + test "auth_params using api_key and app_key" do + expected = [application_key: "abcd", api_key: "1234"] + assert auth_params(%{api_key: "1234", app_key: "abcd"}) == expected + end + + test "auth_params with no auth" do + assert auth_params(%{}) == [] + assert auth_params(nil) == [] + end + + test "process_response with 200" do + assert process_response(%Response{status_code: 200, + headers: %{}, + body: "json"}) == {200, "json"} + assert :meck.validate(Poison) + end + + test "process_response with non-200" do + assert process_response(%Response{status_code: 404, + headers: %{}, + body: "json"}) == {404, "json"} + + assert :meck.validate(Poison) + end + + test "process_resposne_body with nil body" do + assert process_response_body("") == nil + end + + test "process_response_body with content" do + :meck.expect(Poison, :decode!, 1, :decoded_json) + assert process_response_body("json") == :decoded_json + end + + test "process_response with empty body" do + assert process_response(%Response{status_code: 202, + headers: %{}, + body: nil}) == {202, nil} + end + +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() -- cgit v1.2.1