{"id":19620200,"url":"https://github.com/cheerfulstoic/step_wise","last_synced_at":"2025-04-28T03:31:57.121Z","repository":{"id":65250809,"uuid":"570303052","full_name":"cheerfulstoic/step_wise","owner":"cheerfulstoic","description":"`StepWise` is a light wrapper for the parts of your Elixir code which need to be debuggable in production.","archived":false,"fork":false,"pushed_at":"2024-02-13T13:36:58.000Z","size":77,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-05T05:51:08.199Z","etag":null,"topics":["elixir"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cheerfulstoic.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2022-11-24T21:00:41.000Z","updated_at":"2024-02-13T13:36:46.000Z","dependencies_parsed_at":"2023-12-29T19:21:59.781Z","dependency_job_id":"dcba83af-6a6d-4548-bcaf-e8595fc9dc98","html_url":"https://github.com/cheerfulstoic/step_wise","commit_stats":{"total_commits":61,"total_committers":2,"mean_commits":30.5,"dds":"0.016393442622950838","last_synced_commit":"8688f2963d7547c02c0babd7eb09771fa8eee094"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cheerfulstoic%2Fstep_wise","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cheerfulstoic%2Fstep_wise/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cheerfulstoic%2Fstep_wise/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cheerfulstoic%2Fstep_wise/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cheerfulstoic","download_url":"https://codeload.github.com/cheerfulstoic/step_wise/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251246037,"owners_count":21558759,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elixir"],"created_at":"2024-11-11T11:17:25.735Z","updated_at":"2025-04-28T03:31:56.894Z","avatar_url":"https://github.com/cheerfulstoic.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# StepWise\n\n`StepWise` is a light wrapper for the parts of your Elixir code which need to be debuggable in production.  It does this by separating the implementation of your *app logic* from the implementation of how things like *logs, metrics, and traces* are created.\n\nThat means that it:\n\n * ...encourages the breaking down of such code into steps\n * ...requires that each step returns a success or failure state (via standard `{:ok, _}` and `{:error, _}` tuples)\n * ...provides [telemetry](https://hexdocs.pm/telemetry/) events to separate/centralize code for concerns such as logging, metrics, and tracing.\n\nLet's start with some code...\n\n```elixir\ndefmodule MyApp.NotifyCommenters do\n  # This outer `step` isn't neccessary, but it is a useful convention to be able to\n  # track the status of the whole run in addition to individual steps.\n  def run(post, method) do\n    StepWise.step({:ok, post}, \u0026steps/1)\n  end\n\n  def steps(post, method) do\n    {:ok, post}\n    |\u003e StepWise.step(\u0026MyApp.Posts.get_comments/1)\n    |\u003e StepWise.map_step(fn comment -\u003e\n      # get_commenter/1 doesn't return `{:ok, _}`, so we need\n      # to do that here\n\n      {:ok, MyApp.Posts.get_commenter(comment)}\n    end)\n    |\u003e StepWise.step(\u0026notify_users/2, method)\n  end\n\n  def notify_users(user, method) do\n    # ...\n  end\nend\n```\n\nYou might notice that the `step/1` and `map_step/1` functions take function values.  These can be anonymous (i.e. as in `map_step` above), though errors will be clearer when using function values coming from named functions (e.g. `MyApp.Posts.get_comments` and `notify_users` in the example).\n\nThe `step` and `map_step` functions `rescue` / `catch` anything which bubbles up so that you don't have to.  All exceptions/throws will be returned as `{:error, _}` tuples so that they can be handled.  `exit`s, however, are *not* caught on purpose because, as [this Elixir guide](https://elixir-lang.org/getting-started/try-catch-and-rescue.html#exits) says: \"exit signals are an important part of the fault tolerant system provided by the Erlang VM...\"\n\n`{:error, _}` tuples will always be returned with Exception values (all `{:error, _}` tuples without exceptions will be wrapped).  This means that you can:\n\n * ...call `Exception.message` to get a string\n * ...`raise` the exception value if you want to raise the error\n * ...hand the exception to error-collecting services like Sentry, Rollbar, etc...\n * ...pattern match or act upon on the structure and attributes of the exception\n\nYou might also check out [this tweet](https://twitter.com/whatyouhide/status/1266405013460594695) from Andrea Leopardi and the [linked blog post](https://web.archive.org/web/20180414015950/http://michal.muskala.eu/2017/02/10/error-handling-in-elixir-libraries.html#comments-whatyouhide-errors) regarding `Exception` values in error tuples.\n\nIf you are familiar with Elixir's `with`, you may be wondering about it's relation to `StepWise` since `with` also helps you handle a series of statements which could succeed or fail.  See below for more discussion [`StepWise` vs `with`](#stepwise-vs-elixirs-with).\n\n# Telemetry\n\nAs [my colleague](https://github.com/linduxed) put it: *\"Logging definitely feels like one of those areas where it very quickly jumps from 'these sprinkled out log calls are giving us a lot of value' to 'we now have a mess in both code and log output'\"*\n\nCentral to `StepWise` is it's telemetry events to allow actions such as logging, metrics, and tracing be separated as a different concern from your code.  There are three telemetry events:\n\n**`[:step_wise, :step, :start]`**\n\nExecuted when a step starts with the following metadata:\n\n * `id`: A unique ID generated by `:erlang.unique_integer()`\n * `step_func`: The function object given to the `step` / `step_map` function\n * `module`: The module where the `step_func` is defined (for convenience)\n * `func_name`: The name of the `step_func` (for convenience)\n * `input`: The value that was given to the step\n * `context`: The extra argument which is passed down into functions of [arity](https://en.wikipedia.org/wiki/Arity) `2`.\n * `system_time`: The system time when the step was started\n\n**`[:step_wise, :step, :stop]`**\n\nExecuted when a step stop with all of the same metadata as the `start` event, but also with:\n\n * `result`: the value (`{:ok, _}` or `{:error, _}` tuple) that was returned from the step function\n * `success`: A boolean describing if the result was a success (for convenience, based on `result`)\n\nThere is also a `duration` measurement value to give the total time taken by the step (in the `native` time unit)\n\n# Integration With Your App\n\n## Metrics\n\nIf you use `phoenix` you'll get `telemetry_metrics` and a `MyAppWeb.Telemetry` module by default.  In that case you can easily get metrics for time and total counts for all steps that you create:\n\n```elixir\n      summary([:step_wise, :step, :stop, :duration],\n        unit: {:native, :millisecond},\n        tags: [:hostname, :module, :func_name]\n      ),\n      counter([:step_wise, :step, :stop, :duration],\n        unit: {:native, :millisecond},\n        tags: [:hostname, :module, :func_name]\n      ),\n```\n\n## Logging\n\nHere is an example of how you might implement logging for your steps (call `MyApp.StepWiseIntegration.install()` somewhere like your `MyApp.Application.start/2`) (see [this wiki page](https://github.com/cheerfulstoic/step_wise/wiki/Integration-Example) for a more thorough example):\n\n```elixir\ndefmodule MyApp.StepWiseIntegration do\n  def install do\n    :telemetry.attach_many(\n      __MODULE__,\n      [\n        # [:step_wise, :step, :start],\n        [:step_wise, :step, :stop],\n      ],\n      \u0026__MODULE__.handle/4,\n      []\n    )\n  end\n\n   def handle(\n         [:step_wise, :step, :stop],\n         %{duration: duration},\n         %{module: module, func_name: func_name, input: input, result: result},\n         _config\n       ) do\n     case {func_name, result} do\n       {_, {:error, exception}} -\u003e\n         # Getting a string via `Exception.message/1` will mention the `module` and `func_name`\n         # in the string, but if we add them as metadata (depending on where our logs go) we can\n         # more reliably filter to the correct logs.\n         Logger.error(Exception.message(exception), input: input, module: module, func_name: func_name)\n         # Since `StepWise` wraps all errors, calling `Exception.message` will return\n         # information about the if the error was returned/raised and about which\n         # step it came from.  In the code above, calling `Exception.message` on a returned\n         # exception might give us a string like:\n         #   \"There was an error *returned* in MyApp.NotifyCommenters.notify_users/1:\\n\\n\\\"Email server is not available\\\"\"\n\n       # Above we log any errors that occur, but here we only log successes\n       # in the `steps` function so that we don't have a lot of logs.\n       # This serves as a starting point for many, but the point of using `telemetry`\n       # to separate your steps from how they are monitored is that you can organize\n       # your steps and log however you'd like.\n       {:steps, {:ok, value}} -\u003e\n         Logger.info(\n           \"#{module}.#{func_name} *succeeded* in #{duration}\",\n           input: input, result: result,\n           module: module, func_name: func_name\n         )\n     end\n   end\nend\n```\n\n# `StepWise` vs Elixir's `with`\n\nFirst, while `StepWise` has some overlap with `with`'s ability to handle errors, it's attempting to solve a specific problem (improving debugging of production code).  Let's discuss some of the differences:\n\n## `with`\n\nThe `with` clause in Elixir is a way to specify a pattern-matched [\"happy path\"](https://en.wikipedia.org/wiki/Happy_path) for a series of expressions.  The first expression which does not match it's corresponding pattern will be either:\n\n * ...returned from the `with` (if no `else` is given)\n * ...given to a series of pattern matches (using `else`)\n\n`with` also doesn't `rescue` or `catch` for you (for better or worse, the usefulness of that depends on the situation).\n\n`step_wise` requires that each step be either an `{:ok, _}` or `{:error, _}` pattern.\n\n\n## `StepWise`\n\n * ...uses functions to give identification to steps when something goes wrong.\n * ...`rescue`s from exceptions and `catch`es throws.\n * ...*requires* the use of `{:ok, _}` / `{:error, _}` tuples.\n * ...emits `telemetry` events to allow for integration with various debugging tools.\n\n# Tests\n\n`StepWise` supports configuration to disable the wrapping of exceptions and throws:\n\n```elixir\nconfig :step_wise, :wrap_step_function_errors, false\n```\n\nThis is primarily useful just for your `test` environment. Since `StepWise` wraps errors, you would need to test for `StepWise.Error` and `StepWise.StepFunctionError` values, which exist for wrapping and formatting errors in production environments.\n\nAlternatively you might choose to create helpers which allow you to test for errors without needing to worry about the details of `StepWise.Error` and `StepWise.StepFunctionError`.\n\n# Piped Values\n\nThe example above is a primary use-case of chaining together functions in a pipe-like way.  One advantage of pipes in Elixir is that it is easy to follow a single value through a serious of operations.  For example:\n\n```elixir\nsession\n|\u003e get_user()\n|\u003e fetch_posts(10, public: true)\n```\n\nWhile pipes allow for functions of any number of arguments, the `step` and `map_step` functions take function values with a fixed [arity](https://en.wikipedia.org/wiki/Arity).  So in order to allow contextual arguments (like the arguments to `fetch_posts`), it is possible to give `step` and `map_step` a function of arity `2` and then pass in a value (which could be a tuple/map/list/etc... which holds multiple values).  For example:\n\n```\n{:ok, session}\n|\u003e step(\u0026get_user/1)\n|\u003e step(\u0026fetch_posts/2, limit: 10, opts: [public: true])\n```\n\nThis also turns into the single `context` key in the `telemetry` events.\n\nSometimes, however, you may need to pass more complex objects through your `StepWise` pipeline, which might look like...\n\n## State-Based Usage\n\nIn some cases, however, you may want to use a more `GenServer`-like style where you have a state object that is modified along the way:\n\n```elixir\ndef EmailPost do\n  import StepWise\n\n  def run(user_id, post_id) do\n    %{user_id: user_id, post_id: post_id}\n    |\u003e step(\u0026MyApp.Posts.get_comments/1)\n    |\u003e step(\u0026fetch_user_data/1)\n    |\u003e step(\u0026fetch_post_data/1)\n    |\u003e step(\u0026finalize/1)\n  end\n\n  def fetch_user_data(%{user_id: id} = state) do\n    {:ok, Map.put(state, :user, MyApp.Users.get(id))}\n  end\n\n  def fetch_post_data(%{post_id: id} = state) do\n    {:ok, Map.put(state, :post, MyApp.Posts.get(id))}\n  end\n\n  def finalize(%{user: user, post: post}) do\n    # ...\n  end\nend\n```\n\nNote that `import StepWise` is used here.  The first example used the `StepWise` module explicitly to demonstrate the recommendation from the [Elixir guides](https://elixir-lang.org/getting-started/alias-require-and-import.html#import) to prefer `alias` over `import`.  But in self-contained modules you may find the style of `import` preferable.\n\n## Installation\n\nIf [available in Hex](https://hex.pm/docs/publish), the package can be installed\nby adding `step_wise` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:step_wise, \"~\u003e 0.6.1\"}\n  ]\nend\n```\n\n## Documentation\n\nDocumentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)\nand published on [HexDocs](https://hexdocs.pm). Once published, the docs can\nbe found at \u003chttps://hexdocs.pm/step_wise\u003e.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcheerfulstoic%2Fstep_wise","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcheerfulstoic%2Fstep_wise","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcheerfulstoic%2Fstep_wise/lists"}