From 277d8b7d898154ca9017ec76f182254c97570169 Mon Sep 17 00:00:00 2001 From: Michel Boaventura Date: Thu, 17 Apr 2025 16:32:50 -0300 Subject: [PATCH] JSON Module Support for Elixir 1.18 and CI Environment Updates This PR introduces conditional JSON handling and updates our CI environment: * Adds a new PromEx.JSON module that automatically uses Elixir 1.18's built-in JSON module when available, falling back to Jason otherwise * Makes the Jason dependency optional * Adds Elixir 1.18 to the testing matrix * Updates `actions/cache` action to v4 --- .github/workflows/main.yml | 3 ++- CHANGELOG.md | 4 ++++ .../web_app/config/config.exs | 4 ++-- example_applications/web_app/stress_test.exs | 6 +++--- lib/prom_ex/dashboard_renderer.ex | 6 +++--- lib/prom_ex/dashboard_uploader.ex | 2 +- lib/prom_ex/grafana_client.ex | 14 ++++++------- lib/prom_ex/json.ex | 21 +++++++++++++++++++ mix.exs | 2 +- test/prom_ex/dashboard_uploader_test.exs | 2 +- test/prom_ex/grafana_client_test.exs | 4 ++-- test/support/test_app_router.ex | 4 ++-- 12 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 lib/prom_ex/json.ex diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2d375cd9..07ac63be 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: elixir-version: "1.17.2" otp-version: "27.0" - name: Restore cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | deps @@ -53,6 +53,7 @@ jobs: matrix: elixir: - "1.17" + - "1.18" otp: - "26.0" - "27.0" diff --git a/CHANGELOG.md b/CHANGELOG.md index e8228602..7abbb6b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- PromEx now uses Elixir 1.18 default JSON module and falls back to Jason if it is not available. + ## [1.11.0] - 2024-10-24 ### Fixed diff --git a/example_applications/web_app/config/config.exs b/example_applications/web_app/config/config.exs index 48019bbd..4393a65e 100644 --- a/example_applications/web_app/config/config.exs +++ b/example_applications/web_app/config/config.exs @@ -50,8 +50,8 @@ config :logger, :console, format: "$time $metadata[$level] $message\n", metadata: [:request_id] -# Use Jason for JSON parsing in Phoenix -config :phoenix, :json_library, Jason +# Use JSON falling back to Jason for JSON parsing in Phoenix +config :phoenix, :json_library, PromEx.JSON # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/example_applications/web_app/stress_test.exs b/example_applications/web_app/stress_test.exs index 320ae278..0417a6c7 100644 --- a/example_applications/web_app/stress_test.exs +++ b/example_applications/web_app/stress_test.exs @@ -19,7 +19,7 @@ end |> Finch.build(users_path) |> Finch.request(StressTester) -all_users = Jason.decode!(body) +all_users = PromEx.JSON.decode!(body) # Level up all the users slowly all_users @@ -78,7 +78,7 @@ task_2 = age = Faker.random_between(18, 75) data = %{first_name: first_name, last_name: last_name, age: age, points: 0} - payload = Jason.encode!(data) + payload = PromEx.JSON.encode!(data) :post |> Finch.build(users_path, [{"content-type", "application/json"}], payload) @@ -98,7 +98,7 @@ Task.await_many([task_1, task_2], 60_000) |> Finch.build(users_path) |> Finch.request(StressTester) -all_users = Jason.decode!(body) +all_users = PromEx.JSON.decode!(body) all_users |> Enum.shuffle() diff --git a/lib/prom_ex/dashboard_renderer.ex b/lib/prom_ex/dashboard_renderer.ex index 4c1c6866..36a6f3fc 100644 --- a/lib/prom_ex/dashboard_renderer.ex +++ b/lib/prom_ex/dashboard_renderer.ex @@ -107,8 +107,8 @@ defmodule PromEx.DashboardRenderer do end @doc """ - This function will decode the JSON dashboard using `Jason`. If any errors occur during the decoding process, - the struct will be marked as having invalid JSON. + This function will decode the JSON dashboard using JSON if available and fallback to Jason. If any errors + occur during the decoding process, the struct will be marked as having invalid JSON. """ @spec decode_dashboard(__MODULE__.t()) :: __MODULE__.t() def decode_dashboard(%__MODULE__{valid_file?: false} = dashboard_render) do @@ -116,7 +116,7 @@ defmodule PromEx.DashboardRenderer do end def decode_dashboard(%__MODULE__{rendered_file: json_definition} = dashboard_render) do - case Jason.decode(json_definition) do + case PromEx.JSON.decode(json_definition) do {:ok, decoded_dashboard} -> %{dashboard_render | decoded_dashboard: decoded_dashboard, valid_json?: true} diff --git a/lib/prom_ex/dashboard_uploader.ex b/lib/prom_ex/dashboard_uploader.ex index 7837699d..7762cbce 100644 --- a/lib/prom_ex/dashboard_uploader.ex +++ b/lib/prom_ex/dashboard_uploader.ex @@ -126,7 +126,7 @@ defmodule PromEx.DashboardUploader do end defp upload_dashboard(dashboard_definition, grafana_conn, upload_opts, full_dashboard_path) do - dashboard_contents = Jason.encode!(dashboard_definition) + dashboard_contents = PromEx.JSON.encode!(dashboard_definition) case GrafanaClient.upload_dashboard(grafana_conn, dashboard_contents, upload_opts) do {:ok, _response_payload} -> diff --git a/lib/prom_ex/grafana_client.ex b/lib/prom_ex/grafana_client.ex index 1ad97218..8dc02a7a 100644 --- a/lib/prom_ex/grafana_client.ex +++ b/lib/prom_ex/grafana_client.ex @@ -73,7 +73,7 @@ defmodule PromEx.GrafanaClient do dashboard_uid = dashboard_contents - |> Jason.decode!() + |> PromEx.JSON.decode!() |> Map.get("uid") :get @@ -94,7 +94,7 @@ defmodule PromEx.GrafanaClient do headers = grafana_headers(:post, grafana_conn.authorization) payload = - Jason.encode!(%{ + PromEx.JSON.encode!(%{ uid: folder_uid, title: title }) @@ -136,7 +136,7 @@ defmodule PromEx.GrafanaClient do overwrite: true } |> Map.merge(attrs) - |> Jason.encode!() + |> PromEx.JSON.encode!() :put |> Finch.build("#{grafana_conn.base_url}/api/folders/#{folder_uid}", headers, payload) @@ -193,7 +193,7 @@ defmodule PromEx.GrafanaClient do {:time_end, v} -> {:timeEnd, v} {k, v} -> {k, v} end) - |> Jason.encode!() + |> PromEx.JSON.encode!() :post |> Finch.build("#{grafana_conn.base_url}/api/annotations", headers, payload) @@ -204,7 +204,7 @@ defmodule PromEx.GrafanaClient do defp handle_grafana_response(finch_response) do case finch_response do {:ok, %Finch.Response{status: status_code, body: body}} when status_code in [200, 201] -> - {:ok, Jason.decode!(body)} + {:ok, PromEx.JSON.decode!(body)} {:ok, %Finch.Response{status: status_code} = response} -> Logger.warning("Received a #{status_code} from Grafana because: #{inspect(response)}") @@ -236,12 +236,12 @@ defmodule PromEx.GrafanaClient do end defp generate_payload(dashboard_definition, opts) do - dashboard = Jason.decode!(dashboard_definition) + dashboard = PromEx.JSON.decode!(dashboard_definition) opts |> Map.new() |> Map.put(:dashboard, dashboard) - |> Jason.encode!() + |> PromEx.JSON.encode!() end defp lookup_status_code(status_code) do diff --git a/lib/prom_ex/json.ex b/lib/prom_ex/json.ex new file mode 100644 index 00000000..20f05a93 --- /dev/null +++ b/lib/prom_ex/json.ex @@ -0,0 +1,21 @@ +defmodule PromEx.JSON do + @moduledoc false + + # Delegates to JSON in Elixir v1.18+ or Jason for earlier versions + + cond do + Code.ensure_loaded?(JSON) -> + defdelegate decode!(data), to: JSON + defdelegate decode(data), to: JSON + defdelegate encode!(data), to: JSON + + Code.ensure_loaded?(Jason) -> + defdelegate decode!(data), to: Jason + defdelegate encode!(data), to: Jason + + true -> + message = "Missing a compatible JSON library, add `:jason` to your deps." + + IO.warn(message, Macro.Env.stacktrace(__ENV__)) + end +end diff --git a/mix.exs b/mix.exs index 71dda7ae..546ccd1e 100644 --- a/mix.exs +++ b/mix.exs @@ -55,7 +55,7 @@ defmodule PromEx.MixProject do defp deps do [ # Required dependencies - {:jason, "~> 1.4"}, + {:jason, "~> 1.4", optional: true}, {:finch, "~> 0.18"}, {:telemetry, ">= 1.0.0"}, {:telemetry_poller, "~> 1.1"}, diff --git a/test/prom_ex/dashboard_uploader_test.exs b/test/prom_ex/dashboard_uploader_test.exs index 375d5793..fe4fa051 100644 --- a/test/prom_ex/dashboard_uploader_test.exs +++ b/test/prom_ex/dashboard_uploader_test.exs @@ -161,7 +161,7 @@ defmodule PromEx.DashboardUploaderTest do {:ok, body, conn} = Plug.Conn.read_body(conn, []) assert body - |> Jason.decode!() + |> PromEx.JSON.decode!() |> get_in(["dashboard", "title"]) == "My really cool custom title" Plug.Conn.resp(conn, 200, response_payload) diff --git a/test/prom_ex/grafana_client_test.exs b/test/prom_ex/grafana_client_test.exs index 216126d3..0fbced83 100644 --- a/test/prom_ex/grafana_client_test.exs +++ b/test/prom_ex/grafana_client_test.exs @@ -45,7 +45,7 @@ defmodule PromEx.GrafanaClientTest do Bypass.expect(bypass, "POST", "/api/annotations", fn conn -> {:ok, body, conn} = Plug.Conn.read_body(conn, []) - details = Jason.decode!(body) + details = PromEx.JSON.decode!(body) assert details["time"] == 1_640_995_200_000 assert details["tags"] == ["some", "tags"] assert details["text"] == "message" @@ -86,7 +86,7 @@ defmodule PromEx.GrafanaClientTest do Bypass.expect(bypass, "POST", "/api/annotations", fn conn -> {:ok, body, conn} = Plug.Conn.read_body(conn, []) - details = Jason.decode!(body) + details = PromEx.JSON.decode!(body) assert details["dashboardId"] == 1 assert details["panelId"] == 2 assert details["time"] == 1_640_995_200_000 diff --git a/test/support/test_app_router.ex b/test/support/test_app_router.ex index b3986aeb..b056fcea 100644 --- a/test/support/test_app_router.ex +++ b/test/support/test_app_router.ex @@ -56,14 +56,14 @@ defmodule TestApp.PlugRouter do plug PromEx.Plug, prom_ex_module: TestApp.PromEx, path: "/metrics" plug Plug.Telemetry, event_prefix: [:testapp, :plug, :router] - plug Plug.Parsers, parsers: [:json], json_decoder: Jason + plug Plug.Parsers, parsers: [:json], json_decoder: PromEx.JSON plug :match plug :dispatch get "/users/:id" do conn |> put_resp_content_type("application/json") - |> send_resp(200, Jason.encode!(%{"id" => conn.params["id"]})) + |> send_resp(200, PromEx.JSON.encode!(%{"id" => conn.params["id"]})) |> halt() end