<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hans Schnedlitz</title>
    <description>The latest articles on DEV Community by Hans Schnedlitz (@hschne).</description>
    <link>https://dev.to/hschne</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F815921%2Fe57f00dc-ca1d-4dbb-98cd-5d0619865643.png</url>
      <title>DEV Community: Hans Schnedlitz</title>
      <link>https://dev.to/hschne</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hschne"/>
    <language>en</language>
    <item>
      <title>One Version to Rule Them All</title>
      <dc:creator>Hans Schnedlitz</dc:creator>
      <pubDate>Sun, 11 Jan 2026 12:58:00 +0000</pubDate>
      <link>https://dev.to/hschne/one-version-to-rule-them-all-1p26</link>
      <guid>https://dev.to/hschne/one-version-to-rule-them-all-1p26</guid>
      <description>&lt;p&gt;Managing different tools in your modern Ruby on Rails application can be a pain. You definitely use Ruby. You probably use Node, and some package manager - &lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;pnpm&lt;/code&gt; or whatever - to go along with it. Locally, managing versions for all these tools is made easy by tools like &lt;a href="https://mise.jdx.dev/" rel="noopener noreferrer"&gt;Mise&lt;/a&gt; or &lt;a href="https://asdf-vm.com/" rel="noopener noreferrer"&gt;ASDF&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Your local tool versions via Mise
[tools]
ruby = "4.0.0"
node = "25.2.1"
pnpm = "10.18.3"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, managing those versions locally is only part of the equation. You do use CI, right? Your Continuous Integration environments - for example, GitHub Actions - should obviously use the same tool versions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test:
  runs-on: ubuntu-latest
  steps:
    - name: Checkout code
      uses: actions/checkout@v6

    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: '4.0' 
        bundler-cache: true

    - name: Install pnpm
      uses: pnpm/action-setup@v4
      with:
          version: "10.18.3"

    - name: Set up Node
      uses: actions/setup-node@v4
      with:
        node-version: '25.2.1'
        cache: pnpm

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same is true for your deployments. If you use &lt;a href="https://kamal-deploy.org/" rel="noopener noreferrer"&gt;Kamal&lt;/a&gt; that usually means updating your &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ARG RUBY_VERSION=4.0.0
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Base image setup goes here...

FROM base AS build

ARG NODE_VERSION=25.2.1
ARG PNPM_VERSION=10.13.1

# ...

ENV PATH=/usr/local/node/bin:$PATH
RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ &amp;amp;&amp;amp; \
  /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node &amp;amp;&amp;amp; \
  npm install -g pnpm@${PNPM_VERSION} &amp;amp;&amp;amp; \
  rm -rf /tmp/node-build-master

# ...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s a bunch of versions to update across a number of places. Like I said, a bit of a pain to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s Fix This Mess
&lt;/h2&gt;

&lt;p&gt;Obviously, there are ways to improve this situation. Some GitHub actions support reading from a central file. &lt;code&gt;setup-ruby&lt;/code&gt; can read &lt;code&gt;mise.toml&lt;/code&gt; - but &lt;a href="https://github.com/actions/setup-node/pull/1421" rel="noopener noreferrer"&gt;setup-node&lt;/a&gt; can not, at least for now. You can read &lt;a href="https://nodejs.org/api/corepack.html" rel="noopener noreferrer"&gt;pnpm versions directly from your &lt;code&gt;package.json&lt;/code&gt; file&lt;/a&gt; - but that doesn’t really help us, does it now? There just isn’t one standard that allows us to specify each version once, in a single place.&lt;/p&gt;

&lt;p&gt;I’m aware of &lt;a href="https://github.com/jdx/mise-action" rel="noopener noreferrer"&gt;jdx/mise-action&lt;/a&gt;. It’s a fix in theory - at least for GitHub actions, but I’ve found it lacking in practice. For one, it supports caching the tool setup itself - but not caching dependencies installed by those tools. The specialized actions do to that quite well. Also, different steps or workflows only need some tools. It is rare that I need to install all the tools defined in &lt;code&gt;mise.toml&lt;/code&gt; for every workflow and step.&lt;/p&gt;

&lt;p&gt;Now, there’s also Docker and Kamal, and matching versions there is a different story altogether. You can use Kamal build arguments to centralize versions for Docker - but for now that just moves the versions to manage to &lt;code&gt;deploy.yml&lt;/code&gt; instead of our &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder:
  arch: amd64
  cache:
    type: gha
  args:
    RUBY_VERSION: "4.0"
    NODE_VERSION: "25.2.1"
    PNPM_VERSION: "10.18.3"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So what gives? Here’s what I ended up with. We go back to good, old, individual version files for each tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .ruby-version
4.0.0


# .node-version
25.2.1


# package.json
{
  "private": true,
  "type": "module",
  "packageManager": "pnpm@10.18.3",
  "devDependencies": { ... },
  "dependencies": { ... }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, hear me out. Here’s why this works.&lt;/p&gt;

&lt;p&gt;Let’s talk about GitHub actions first. Obviously, every specialized setup tool supports reading each specialized version file out of the box. Easy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      # Reads from .ruby-version automatically
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      # Reads from package.json automatically
      - name: Install pnpm
        uses: pnpm/action-setup@v4

      - name: Set up Node
        uses: actions/setup-node@v6
        with:
          node-version-file: .node-version
          cache: pnpm

      - name: Install JavaScript dependencies
        run: pnpm install

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mise is a beast and can work with almost anything you throw at it. Including &lt;code&gt;package.json&lt;/code&gt; and specialized version files - the latter out of the box. By using a configuration like this your local setup is now also covered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[tools]
# Picked up from .ruby-version and .node-version

[settings]
experimental = true

[hooks]
# Enabling corepack will install the `pnpm` package manager specified in your package.json
postinstall = 'npx corepack enable'

[env]
_.path = ['/node_modules/.bin']

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That leaves Kamal. And here we can do something fun. Because our version files are simple, we can read from them directly without much of a hassle. And because Kamal supports ERB templating, we can do this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Just read the versions from the version files, easy
builder:
  arch: amd64
  cache:
    type: gha
  args:
    RUBY_VERSION: &amp;lt;%= File.read('.ruby-version').strip %&amp;gt;
    NODE_VERSION: &amp;lt;%= File.read('.node-version').strip %&amp;gt;
    PNPM_VERSION: &amp;lt;%= JSON.parse(File.read('package.json'))['packageManager'].split('@').last %&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want to upgrade Ruby? Change &lt;code&gt;.ruby-version&lt;/code&gt;. Want to upgrade Node? Change &lt;code&gt;.node-version&lt;/code&gt;. Update pnpm? Change the version in your &lt;code&gt;package.json&lt;/code&gt;. Any change will be picked up across all your environments. It’s simple, and it works beautifully.&lt;/p&gt;

</description>
      <category>ruby</category>
    </item>
    <item>
      <title>Ruby on Rails, Claude Code and Worktrees</title>
      <dc:creator>Hans Schnedlitz</dc:creator>
      <pubDate>Thu, 10 Jul 2025 11:26:00 +0000</pubDate>
      <link>https://dev.to/hschne/ruby-on-rails-claude-code-and-worktrees-848</link>
      <guid>https://dev.to/hschne/ruby-on-rails-claude-code-and-worktrees-848</guid>
      <description>&lt;p&gt;Here’s how I use Claude Code to develop Ruby on Rails applications. Mostly medium sized ones that have been around for a while. No green-field, everything-is-possible, agents-go-wild vibe coding for me. I’ll leave that to others.&lt;/p&gt;

&lt;p&gt;I don’t know what I’m doing. Wait, that’s not entirely right. I know what I’m doing when it comes to Rails. I don’t know what I’m doing when it comes to Agentic Coding. But that’s okay, &lt;a href="https://worksonmymachine.substack.com/p/nobody-knows-how-to-build-with-ai" rel="noopener noreferrer"&gt;nobody does&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We’re Working With
&lt;/h2&gt;

&lt;p&gt;The apps I work on are as stock-standard Ruby on Rails as it gets. Some use Bootstrap, some use Tailwind. Some use Postgres, some SQLite. Some have trace amounts of Javascript in them. However, they all follow the default Rails architectural patterns: models, views, and controllers. There are no service objects and no event-based systems.&lt;/p&gt;

&lt;p&gt;In this context, Claude works great. It eliminates drudgery. I’ve used it to update a bunch of views after extensive model changes. I’ve used it to make significant updates to models and their validations. I’ve added &lt;em&gt;a lot&lt;/em&gt; of translations with Claude. I disable all permission checks, and &lt;a href="https://lucumr.pocoo.org/2025/6/12/agentic-coding/" rel="noopener noreferrer"&gt;I’m not alone&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It’s nice. While Claude works on boring stuff, you’re free to do other things. You could grab a coffee. You could play with your kids. Or you could work on the hard stuff that Claude sucks at. You could also start more instances of Claude Code to work on even _more things.&lt;/p&gt;

&lt;p&gt;However, that requires some measure of isolation. You certainly don’t want to work on the same code as Claude while it’s doing its thing - that way lies pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolation with Worktrees
&lt;/h2&gt;

&lt;p&gt;Using &lt;a href="https://git-scm.com/docs/git-worktree" rel="noopener noreferrer"&gt;git-worktrees&lt;/a&gt; you create a new branch that is contained within a specific folder. Then, you start an instance of Claude Code in that folder, and it won’t interfere with the rest of your codebase. Voila, you’ve achieved perfect isolation. I didn’t figure this one out myself, by the way. I just &lt;a href="https://docs.anthropic.com/en/docs/claude-code/common-workflows#run-parallel-claude-code-sessions-with-git-worktrees" rel="noopener noreferrer"&gt;read the manual&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I like to keep the &lt;code&gt;/worktrees&lt;/code&gt; directory within my project folder. I found that I can make my life a lot easier by doing some scripting and by using &lt;a href="https://github.com/tmux/tmux" rel="noopener noreferrer"&gt;Tmux&lt;/a&gt;. Here’s a simplified version of a script I’m using to start a new Claude Code instance with a new prompt, on a new branch, in it’s own worktree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WORKTREES_DIR="./worktrees"

create_worktree() {
  local feature_description="$1"
  local prompt="$2"

  local text=${feature_description// /-}
  text=$(echo "$text" | tr '[:upper:]' '[:lower:]')
  local date
  date=$(date +"%y-%m")
  local branch_name="$date-$text"

  local worktree_path="$WORKTREES_DIR/$branch_name"

  tmux split-window -t "worktrees" -c "$worktree_path" "echo '$prompt' | claude --dangerously-skip-permissions"
}

create_worktree "$@"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So here’s what I usually do. I have a Tmux session running for my Rails project. When I want to offload some work to Claude I start a new Tmux Window/Pane using this little helper script and let it go wild. I might check in periodically, while I keep working on a different branch in my main window. Mostly I rely on &lt;a href="https://docs.anthropic.com/en/docs/claude-code/hooks" rel="noopener noreferrer"&gt;hooks&lt;/a&gt;, though, to notify me when Claude is done.&lt;/p&gt;


  


&lt;p&gt;Now, what to do with the code that our friendly AI spit out?&lt;/p&gt;

&lt;h2&gt;
  
  
  Review Time
&lt;/h2&gt;

&lt;p&gt;With Rails, I found that automated checks only go so far. I like to test things - especially if there’s view changes involved. I could have Claude open a PR and review that, but that doesn’t do it for me. I want to switch to the branch, run a diff, and start Rails on the branch to verify the results.&lt;/p&gt;

&lt;p&gt;Once again, we can leverage Tmux - and it’s good friend &lt;a href="https://github.com/tmuxinator/tmuxinator" rel="noopener noreferrer"&gt;Tmuxinator&lt;/a&gt;. I create a new local &lt;code&gt;.tmuxinator&lt;/code&gt; file in each of my projects. This gives me the flexibility I need. Every project is different.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: worktree
root: &amp;lt;%= @args[0] %&amp;gt;
on_project_start: yarn install

windows:
  - editor:
      panes:
        - vim: nvim
  - background:
      layout: tiled
      panes:
        - server: PORT=$(nextport 3000) bundle exec foreman start -f Procfile.dev

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To avoid port conflicts with already running Rails servers, I use a simple script to &lt;a href="https://github.com/hschne/dotfiles/blob/master/home/.scripts/nextport" rel="noopener noreferrer"&gt;get the next open port&lt;/a&gt;. Then, we can start reviewing by running &lt;code&gt;tmuxinator start --append worktree &amp;lt;path-to-worktree&amp;gt;&lt;/code&gt;.&lt;/p&gt;


  


&lt;p&gt;Now I can make manual changes if necessary using the new editor instance, or tell Claude to open a PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next?
&lt;/h2&gt;

&lt;p&gt;I don’t know. Nobody knows; the landscape changes quickly. For Rails apps, within the constraints I’ve outlined, this works wonderfully for now. I’ve run up to three instances of Claude code in parallel. Now, those are rookie numbers, and I gotta get those up - I’m working on it. There are limits on how much you can parallelize in any given code base.&lt;/p&gt;

&lt;p&gt;You can find the full script I use to manage worktrees and Claude Code in &lt;a href="https://github.com/hschne/claude-worktree" rel="noopener noreferrer"&gt;this repository&lt;/a&gt;. Have fun!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>claude</category>
      <category>git</category>
    </item>
    <item>
      <title>Multi Select with Hotwire Combobox</title>
      <dc:creator>Hans Schnedlitz</dc:creator>
      <pubDate>Sat, 05 Jul 2025 10:50:00 +0000</pubDate>
      <link>https://dev.to/hschne/multi-select-with-hotwire-combobox-50b7</link>
      <guid>https://dev.to/hschne/multi-select-with-hotwire-combobox-50b7</guid>
      <description>&lt;p&gt;Recently, I put some work into lowering the barrier to contributing talks to &lt;a href="https://www.rubyevents.org/" rel="noopener noreferrer"&gt;RubyEvents&lt;/a&gt;. Not entirely out of the kindness of my heart, I might add. As the organizer of the &lt;a href="https://www.meetup.com/vienna-rb/" rel="noopener noreferrer"&gt;Vienna.rb Ruby Meetup&lt;/a&gt;, I contribute talk recordings every couple of months.&lt;/p&gt;

&lt;p&gt;Previously, that meant updating a bunch of YAML files, and who enjoys manually updating a bunch of YAML? I certainly don’t.&lt;/p&gt;

&lt;h2&gt;
  
  
  Less YAML, More Forms
&lt;/h2&gt;

&lt;p&gt;RubyEvents now provides &lt;a href="https://www.rubyevents.org/templates/new" rel="noopener noreferrer"&gt;a simple form&lt;/a&gt; where you can fill in event and talk details and automatically generate the needed YAML. Then you just copy paste and open a PR.&lt;/p&gt;

&lt;p&gt;Part of that form is the selection of speakers. In the initial implementation, you would simply enter a comma-separated list of names.&lt;/p&gt;

&lt;p&gt;Now, wouldn’t it be nice if you could search and get speakers auto-suggested? Yes, it would, and yes, indeed it is! Following &lt;a href="https://github.com/rubyevents/rubyevents/pull/811#issuecomment-2986576265" rel="noopener noreferrer"&gt;Adrian Poly’s suggestion&lt;/a&gt;, I used Hotwire Combobox. This is what the final result looks like.&lt;/p&gt;


  


&lt;p&gt;I’m not going to lie, implementation wasn’t as straightforward as I would have hoped. Hotwire Combobox isn’t huge on documentation - “just read the source code” is the motto here. If you’re as stumped as I was, this post may help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basics First
&lt;/h2&gt;

&lt;p&gt;To simplify the input of speaker names and provide a pleasant user experience, we have a couple of requirements.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We want to search for speakers by name&lt;/li&gt;
&lt;li&gt;We want to be able to select multiple speakers&lt;/li&gt;
&lt;li&gt;We must be able to add new speakers by entering arbitrary text&lt;/li&gt;
&lt;li&gt;Speaker names should be used in the final YAML&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s tackle these one by one. We start simple. By following &lt;a href="https://github.com/josefarias/hotwire_combobox" rel="noopener noreferrer"&gt;the instructions in the README&lt;/a&gt; or &lt;a href="https://justin.searls.co/posts/hotwirecombobox-is-pretty-damn-slick/" rel="noopener noreferrer"&gt;Justin Searl’s excellent tutorial&lt;/a&gt;, we can get up and running quickly. Let’s add basic auto-completion for speaker names first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/controllers/templates_controller.rb
def speakers_search
  @speakers = Speaker.canonical
  @speakers = @speakers.ft_search(search_query) if search_query
  @speakers = @speakers.limit(100)
end


# app/views/templates/speakers_search.turbo_stream.erb
&amp;lt;%= async_combobox_options @speakers %&amp;gt;


# app/views/templates/new.html.erb --&amp;gt;
&amp;lt;%= form.combobox :speakers, speakers_search_templates_path %&amp;gt;


# app/models/speaker.rb
def to_combobox_display
  name
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works great. We can search for and auto-complete speaker names.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx60d1we5o66f275fnkrb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx60d1we5o66f275fnkrb.png" alt="Basic autocompletion with Hotwire Combobox" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi Selection with Hotwire Combobox
&lt;/h2&gt;

&lt;p&gt;Now, let’s tackle multi-selection. Sadly, there are no pointers on how to implement this on the &lt;a href="https://hotwirecombobox.com/" rel="noopener noreferrer"&gt;Hotwire Combobox Homepage&lt;/a&gt;. Time to go spelunking in the source code. Thankfully, there are &lt;a href="https://github.com/josefarias/hotwire_combobox/tree/main/test/dummy/app/views/comboboxes" rel="noopener noreferrer"&gt;tons of examples in the test/dummy app&lt;/a&gt;. One of them shows us how to add multi-selection.&lt;/p&gt;

&lt;p&gt;We need to pass &lt;code&gt;multiselect_chip_src&lt;/code&gt; and use the &lt;code&gt;combobox_selection_for_chips&lt;/code&gt; helper to render selected items.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/views/templates/new.html.erb --&amp;gt;
&amp;lt;%= form.combobox :speakers,
    speakers_search_templates_path,
    multiselect_chip_src: speakers_search_chips_templates_path %&amp;gt;


# app/controllers/templates_controller.rb
def speakers_search_chips
  @speakers = Speaker.find(params[:combobox_values].split(","))
  render turbo_stream: helpers.combobox_selection_chips_for(@speakers)
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hotwire Combobox does the heavy lifting for us. We get a nice looking result without much of a fuss.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0gf5ig2lh9we8yysd56.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0gf5ig2lh9we8yysd56.png" alt="Multi Selection with Hotwire Combobox" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Free Text and Combobox Value
&lt;/h2&gt;

&lt;p&gt;This solution already works wonderfully for existing speakers. However, remember that we also want to be able to add new speakers. To do so, we’ll need to add &lt;code&gt;free_text&lt;/code&gt; and &lt;code&gt;name_when_new&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We also need to make some modifications to the controller. A free-text name isn’t an actual speaker model - Hotwire Combobox can’t work with that. So we’ll need to do some manual mapping. This is based on these &lt;a href="https://github.com/josefarias/hotwire_combobox/blob/main/test/dummy/app/controllers/state_chips_controller.rb" rel="noopener noreferrer"&gt;helpful examples in the source code&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/views/templates/new.html.erb 
&amp;lt;%= form.combobox :speakers,
    speakers_search_templates_path,
    multiselect_chip_src: speakers_search_chips_templates_path,
    name_when_new: "template[speakers]",
    free_text: true %&amp;gt;


# app/controllers/templates_controller.rb
def speakers_search_chips
  @speakers = params[:combobox_values].split(",").map do |value|
    Speaker.find_by(id: value) || OpenStruct.new(to_combobox_display: value, id: value)
  end
  render turbo_stream: helpers.combobox_selection_chips_for(@speakers)
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the last missing piece is the ability to control the checkbox’s value. By default, Hotwire Combobox uses the ID of records. We allow &lt;code&gt;free_text&lt;/code&gt;, so our combobox values may be a mix of IDs and Strings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;input 
  type="hidden"
  name="template[speakers]" 
  value="4,2887,784,Your Name?"
  id=...
&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To generate a YAML with speaker names, we are only interested in their names. Now, we could do some mapping in the controller, but there is a more straightforward way. We can provide a proc to the combobox helper to map the value from IDs to whatever we need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/views/templates/speakers_search.turbo_stream.erb
&amp;lt;%= async_combobox_options @speakers, value: proc { |element| element.name } %&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are curious about more details, check out the &lt;a href="https://github.com/rubyevents/rubyevents/pull/828" rel="noopener noreferrer"&gt;PR to RubyEvents&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>hotwire</category>
      <category>rails</category>
    </item>
    <item>
      <title>Using Jekyll with Esbuild</title>
      <dc:creator>Hans Schnedlitz</dc:creator>
      <pubDate>Fri, 09 Feb 2024 09:00:00 +0000</pubDate>
      <link>https://dev.to/hschne/using-jekyll-with-esbuild-1ggo</link>
      <guid>https://dev.to/hschne/using-jekyll-with-esbuild-1ggo</guid>
      <description>&lt;p&gt;I know. What an unholy union. Why would anyone do this? Why would &lt;em&gt;anyone&lt;/em&gt; want that?&lt;/p&gt;

&lt;p&gt;Well, first for science. Obviously.&lt;/p&gt;

&lt;p&gt;Second, believe it or not, there are &lt;em&gt;actual&lt;/em&gt; good reasons for combining Esbuild with Jekyll. I like Jekyll. It’s mature, has many plugins, and a vibrant ecosystem. Also, it’s built on Ruby, and Ruby is fantastic. Most importantly, it’s simple.&lt;/p&gt;

&lt;p&gt;But.&lt;/p&gt;

&lt;p&gt;Sometimes, you want that extra bit of visual oomph. Sometimes, you want to do weird or cool things, and a bundler might be necessary under those circumstances. Esbuild is modern and efficient. If you’re working with Ruby on Rails, you might already be used to it. Coincidentally, it’s also simple. Relatively speaking.&lt;/p&gt;

&lt;p&gt;If you look at it like that, maybe Jekyll and Esbuild are meant to work together after all?&lt;/p&gt;

&lt;p&gt;Why not Hugo? Or Bridgetown? Or any other static site generator that’s not Jekyll? Look, I don’t know what else to say. For science. I like Jekyll. It works for me 🙃&lt;/p&gt;

&lt;h2&gt;
  
  
  What we’ll do
&lt;/h2&gt;

&lt;p&gt;The idea is this. We want to use Esbuild for bundling JS and CSS and let Jekyll take care of the rest. We’ll also add some plugins to Esbuild (&lt;a href="https://github.com/postcss/autoprefixer"&gt;autoprefixer&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/esbuild-sass-plugin"&gt;build-sass-plugin&lt;/a&gt;, &lt;a href="https://postcss.org/"&gt;postcss&lt;/a&gt;). First, so we can keep using the SCSS that Jekyll already provides, second for demonstration purposes.&lt;/p&gt;

&lt;p&gt;We’ll also make sure we end up with a pleasant developer experience. Jekyll and Esbuild might not want to play nice, but we’ll make them 😈&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting set up
&lt;/h2&gt;

&lt;p&gt;Heads up! This guide assumes you have Ruby, Bundler, and Node set up on your machine. Also, Jekyll should already be installed. If that’s not the case, take care of that before you continue.&lt;/p&gt;

&lt;p&gt;We’ll forego any plugins and start with an empty Jekyll scaffold to keep things simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jekyll new jekyll-esbuild &lt;span class="nt"&gt;--blank&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;jekyll-esbuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s create an empty Javascript file for demonstration purposes. Let’s also install Esbuild and the plugins we are going to use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;assets/javascript &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;touch &lt;/span&gt;assets/javascript/main.js
npm i &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;--save-exact&lt;/span&gt; esbuild
npm i &lt;span class="nt"&gt;-D&lt;/span&gt; autoprefixer esbuild-sass-plugin postcss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we run &lt;code&gt;jekyll serve --watch&lt;/code&gt; we should see &lt;em&gt;something&lt;/em&gt; on &lt;a href="http://localhost:4000"&gt;localhost:4000&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--O7JW0fQt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://hansschnedlitz.com/assets/images/posts/2024-jekyll-esbuild/jekyll.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--O7JW0fQt--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://hansschnedlitz.com/assets/images/posts/2024-jekyll-esbuild/jekyll.webp" alt="A Jekyll site" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundling Assets with Esbuild
&lt;/h2&gt;

&lt;p&gt;To use Esbuild with our CSS plugins, using the command line won’t do. We’ll have to create a small build script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/build.mjs&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;esbuild&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;esbuild&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sassPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;esbuild-sass-plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;postcss&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postcss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;autoprefixer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autoprefixer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;esbuild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assets/css/main.scss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assets/javascript/main.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;outdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_site/assets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;sassPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;postcss&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;autoprefixer&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;loadPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_sass&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we update our &lt;code&gt;package.json,&lt;/code&gt; we can build our assets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node scripts/build.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we were to run &lt;code&gt;npm run build&lt;/code&gt; now, we’d get an error. Our &lt;code&gt;assets/main.scss&lt;/code&gt; still contains YML markup that we need to remove.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- &amp;lt;&amp;lt;&amp;lt; DELETE ME
--- &amp;lt;&amp;lt;&amp;lt;

@import "base";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, we should be able to build assets without any issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Jekyll
&lt;/h3&gt;

&lt;p&gt;Now, if you keep a close eye on the &lt;code&gt;_site&lt;/code&gt; folder and make some changes to any watched files, you’ll notice that your built files will be changed. Makes sense because, as things stand, Jekyll still feels responsible for assets, thus overwriting or deleting the files created by Esbuild.&lt;/p&gt;

&lt;p&gt;Let’s fix that. The simplest way to tell Jekyll to not worry about CSS and JS assets is to exclude the respective folders by updating &lt;code&gt;_config.yml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;_sass&lt;/span&gt; &lt;span class="c1"&gt;# Let build handle CSS&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;assets/css&lt;/span&gt; &lt;span class="c1"&gt;# Let build handle CSS&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;assets/javascript&lt;/span&gt; &lt;span class="c1"&gt;# Let esuild handle JS&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;scripts&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;package.json&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;package-lock.json&lt;/span&gt;
&lt;span class="na"&gt;keep_files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;assets/css&lt;/span&gt; &lt;span class="c1"&gt;# Let build handle CSS&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;assets/javascript&lt;/span&gt; &lt;span class="c1"&gt;# Let esuild handle JS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that we also told Jekyll to not delete CSS and JS assets when rebuilding. We also excluded some additional files that our Jekyll site doesn’t need. See &lt;a href="https://jekyllrb.com/docs/configuration/options/"&gt;configuration options&lt;/a&gt; if you need more details.&lt;/p&gt;

&lt;p&gt;This won’t do if you’re using a theme. By excluding asset folders, theme styles won’t be processed appropriately. There are ways to fix that, but it’s a bit much for this blog post.&lt;/p&gt;

&lt;p&gt;Let’s bundle and serve again to make sure everything is still working.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
jekyll serve &lt;span class="nt"&gt;--watch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changing anything (e.g. CSS) will no longer overwrite files created by Esbuild. Success! Unfortunately, our changes won’t be reflected on your site. Let’s change that by adding watch mode to Esbuild.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improving Developer Experience
&lt;/h3&gt;

&lt;p&gt;Until the beginning of 2023, adding watch mode was a simple matter of adding &lt;code&gt;watch: true&lt;/code&gt; to the arguments of Esbuild. Now, it’s a bit different. I opted to change the script behavior based on input arguments, but you can also create a separate script.&lt;/p&gt;

&lt;p&gt;The updated &lt;code&gt;scripts/build.mjs&lt;/code&gt; looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--watch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;esbuild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;context&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Watching!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rebuild&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Build done!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After updating &lt;code&gt;package.json&lt;/code&gt; once again, we can start watching our file changes by running &lt;code&gt;npm run watch&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "devDependencies": {
    ...
  },
  "scripts": {
    "watch": "node scripts/build.mjs --watch"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’re already using Ruby anyway, so we might as well use &lt;a href="https://github.com/ddollar/foreman"&gt;Foreman&lt;/a&gt; to run everything we need with a simple command. Note that I also added &lt;a href="https://browsersync.io/"&gt;browser-sync&lt;/a&gt; for live reloading to the &lt;code&gt;Procfile&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jekyll: jekyll serve --watch
build: npm run watch
browser: browser-sync start --proxy localhost:4000 --files "**/*"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;foreman start&lt;/code&gt; and your site should reload whenever you change your CSS, JS, or content. Mission accomplished!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Simplest Static Site Generator</title>
      <dc:creator>Hans Schnedlitz</dc:creator>
      <pubDate>Sun, 07 Jan 2024 17:42:16 +0000</pubDate>
      <link>https://dev.to/hschne/the-simplest-static-site-generator-458f</link>
      <guid>https://dev.to/hschne/the-simplest-static-site-generator-458f</guid>
      <description>&lt;p&gt;Sometimes, you want to build a simple HTML page and populate it with some data. You may have some JSON lying around and want to make a simple website to visualize its contents. Or perhaps you want to show off &lt;a href="https://www.hansschnedlitz.com/bookshelf/"&gt;how many books you read&lt;/a&gt; in the last few years.&lt;/p&gt;

&lt;p&gt;In any case, we’re talking about a small set of data and a &lt;em&gt;single&lt;/em&gt; HTML page here. Would you use a static site generator for that? Which one? Jekyll? Hugo, Gatsby? Isn’t that overkill for what we’re trying to do?&lt;/p&gt;

&lt;p&gt;I was pondering questions like this when I came across &lt;a href="https://pandoc.org/MANUAL.html#templates"&gt;Pandoc Templates&lt;/a&gt;. Turns out there is a way to generate static sites that is much simpler than most other options out there.&lt;/p&gt;

&lt;p&gt;Alright, this might not be the absolutely simplest way to generate a static site. You got me. I bet someone, somewhere builds their static sites using a one-line Perl script. Jokes aside, let me know if you find an even simpler approach to building static sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter The Template
&lt;/h2&gt;

&lt;p&gt;A Pandoc template is not much to look at. It’s just an HTML file filled with some special syntax sprinkled in. Syntax that allows you to write &lt;a href="https://pandoc.org/MANUAL.html#conditionals"&gt;conditionals&lt;/a&gt; or &lt;a href="https://pandoc.org/MANUAL.html#for-loops"&gt;loops&lt;/a&gt; as well as do some other stuff. Let’s say we want to list authors and their respective books to simplify things. To do so, let’s create &lt;code&gt;template.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"date"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"$date-meta$"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;$title$&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;section&amp;gt;&lt;/span&gt;
      $for(authors)$
      &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"author"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;$authors.name$&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        $for(authors.books)$
        &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"book"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"$authors.books.link$"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;$authors.books.name$&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        $endfor$
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
      $endfor$
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have a template, we must populate it with some data. I’ve found the most uncomplicated way to do so is to create a markdown file that contains the data as &lt;a href="https://pandoc.org/chunkedhtml-demo/8.10-metadata-blocks.html#extension-yaml_metadata_block"&gt;metadata&lt;/a&gt;. We keep our data in &lt;code&gt;authors.md&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authors&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;their&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Books"&lt;/span&gt;
&lt;span class="na"&gt;authors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;James S.A. Corey&lt;/span&gt;
    &lt;span class="na"&gt;books&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Leviathan Wakes&lt;/span&gt;
        &lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://www.goodreads.com/book/show/8855321-leviathan-wakes&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Leviathan Falls&lt;/span&gt;
        &lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://www.goodreads.com/book/show/28335699-leviathan-falls&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Margaret Atwood&lt;/span&gt;
    &lt;span class="na"&gt;books&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The Handmaid's Tale&lt;/span&gt;
        &lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://www.goodreads.com/book/show/38447.The_Handmaid_s_Tale&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I found Markdown documents with embedded metadata easy enough to work with. If you prefer specific JSON or YML files, you can use the &lt;code&gt;--metadata-file&lt;/code&gt; flag to pass them to your template. See also the other &lt;a href="https://pandoc.org/MANUAL.html#reader-options"&gt;reader options&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then, run &lt;a href="https://pandoc.org/"&gt;Pandoc&lt;/a&gt; to generate your static site. And that’s it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pandoc &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="nt"&gt;--template&lt;/span&gt; template.html authors.md &lt;span class="nt"&gt;-o&lt;/span&gt; index.html

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ll take Pandoc over other options every day of the week for this particular use case. What about you?&lt;/p&gt;

&lt;h2&gt;
  
  
  I Don’t Want to Install Pandoc!
&lt;/h2&gt;

&lt;p&gt;I know, right? I don’t want to either. Let’s use Docker instead. Put this into your &lt;code&gt;.aliases&lt;/code&gt; and use &lt;code&gt;pandock&lt;/code&gt; rather than &lt;code&gt;pandoc&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;pandock&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'docker run --rm -v "$(pwd):/data" -u $(id -u):$(id -g) pandoc/latex'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>html</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Continuous Deployment with GitHub Actions and Kamal</title>
      <dc:creator>Hans Schnedlitz</dc:creator>
      <pubDate>Sun, 07 Jan 2024 17:27:03 +0000</pubDate>
      <link>https://dev.to/hschne/continuous-deployment-with-github-actions-and-kamal-104a</link>
      <guid>https://dev.to/hschne/continuous-deployment-with-github-actions-and-kamal-104a</guid>
      <description>&lt;p&gt;&lt;a href="https://kamal-deploy.org/"&gt;Kamal&lt;/a&gt; is a wonderfully simple way to deploy your applications anywhere. It will also be &lt;a href="https://github.com/rails/rails/issues/50441"&gt;included by default in Rails 8&lt;/a&gt;. Kamal is trivial, but I don’t recommend using it on your development machine.&lt;/p&gt;

&lt;p&gt;From experience working on an oldish laptop, I can tell you that building Docker images locally is not fun. Also, why would you, when GitHub Actions are for free!&lt;/p&gt;

&lt;p&gt;In this post, I’ll show you how to build a simple CI pipeline with Kamal. We’ll create an application image and deploy it on every push. We’ll also add some simple image caching to speed up the workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Complete Workflow
&lt;/h3&gt;

&lt;p&gt;This is what we’ll end up with at the end of this post.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event_name == 'push' &amp;amp;&amp;amp; github.ref == 'refs/heads/main' }}&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DOCKER_BUILDKIT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildx&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Docker Hub&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hschne&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_REGISTRY_KEY }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set Tag&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tag&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "tag=$(git rev-parse "$GITHUB_SHA")" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build image&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.buildx.outputs.name }}&lt;/span&gt;
          &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;"service=anonymous-location"&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;"hschne/anonymous-location:latest"&lt;/span&gt;
            &lt;span class="s"&gt;"hschne/anonymous-location:${{ steps.tag.outputs.tag }}"&lt;/span&gt;
          &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
          &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webfactory/ssh-agent@v0.7.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ssh-private-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Ruby&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy command&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec kamal deploy --skip-push&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;RAILS_MASTER_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RAILS_MASTER_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;KAMAL_REGISTRY_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_REGISTRY_KEY }}&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s something. Well, nobody ever said GitHub actions are succinct. Let’s look at what’s going on here step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step By Step
&lt;/h2&gt;

&lt;p&gt;The workflow above is based on &lt;a href="https://dev.to/haukot/how-to-cache-mrsk-deployments-in-ci-52h9"&gt;the one in this post&lt;/a&gt;. Unfortunately, that post is so old that it still refers to Kamal as Mrsk. I had to make some adjustments to how the image built and deployed.&lt;/p&gt;

&lt;p&gt;I won’t go into details on GitHub Actions specifics. If you’re new to GitHub Actions or unfamiliar with one particular piece of syntax, I recommend you check out &lt;a href="https://docs.github.com/en/actions"&gt;the documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To build and deploy our Rails application, we need to provide GitHub actions with three &lt;a href="https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions"&gt;secrets&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RAILS_MASTER_KEY&lt;/code&gt;: The master key to your Rails application credentials.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DOCKER_REGISTRY_KEY&lt;/code&gt;: The API token for pushing and pulling from your container registry.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SSH_PRVIATE_KEY&lt;/code&gt;: The SSH key for accessing the server where your app is deployed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ll also need to set some environment variables. We are building a production image. We’ll also need to instruct the Docker build step to use Docker Buildkit, as that is one of the requirements of Kamal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;DOCKER_BUILDKIT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt; 

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our deployment workflow needs to do a couple of things. First, we need to check out the application source code. Next, we’ll log into Docker Hub to push our image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildx&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v2&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Docker Hub&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hschne&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_REGISTRY_KEY }}&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kamal uses the git hash of the latest commit to determine which image to deploy, so image tags must match git commit hashes. We define this tag with a separate workflow step.&lt;/p&gt;

&lt;p&gt;We use the &lt;a href="https://github.com/docker/build-push-action"&gt;docker/build-push-action&lt;/a&gt; to build the application image. In addition to setting the correct tag, the image build step must also provide a label matching your service name. Because the image should be pushed to your container registry, we set &lt;code&gt;push: true&lt;/code&gt;, and because we want ludicrous build speed we instruct the build step to utilize the GitHub Actions cache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set Tag&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tag&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;echo "tag=$(git rev-parse "$GITHUB_SHA")" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build image&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v5&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.buildx.outputs.name }}&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;"service=service-name"&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;"user/image-name:latest"&lt;/span&gt;
      &lt;span class="s"&gt;"user/image-name:${{ steps.tag.outputs.tag }}"&lt;/span&gt;
    &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
    &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max&lt;/span&gt; 

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the image has been built and pushed, you only need to trigger the deployment using Kamal. We use the &lt;a href="https://github.com/webfactory/ssh-agent"&gt;webfactory/ssh-agent&lt;/a&gt; to establish a connection to our production server. After installing the required Ruby dependencies, it’s only a matter of running Kamal. As the image is already built and pushed, we use the &lt;code&gt;--skip-push&lt;/code&gt; flag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;webfactory/ssh-agent@v0.7.0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ssh-private-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SSH_PRIVATE_KEY }}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Ruby&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy command&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec kamal deploy --skip-push&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that’s it! If you’ve enjoyed this post or have any other tips on how to use Kamal together with GitHub Actions, let me know! 🙂&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Making the Most of Your Logs in Rails</title>
      <dc:creator>Hans Schnedlitz</dc:creator>
      <pubDate>Wed, 15 Mar 2023 12:06:34 +0000</pubDate>
      <link>https://dev.to/appsignal/making-the-most-of-your-logs-in-rails-4503</link>
      <guid>https://dev.to/appsignal/making-the-most-of-your-logs-in-rails-4503</guid>
      <description>&lt;p&gt;Most people only realize the necessity of logs when they need them the most. But when your application breaks, user complaints start flooding in, and you have no clue how to fix it, it's too late to add some log messages that might have helped.&lt;/p&gt;

&lt;p&gt;Good logs pay for themselves tenfold. They make it a breeze to diagnose those tricky bugs, and if you do logs right, they can alert you of issues even before your users notice. But what does it mean to 'do logging right'?&lt;/p&gt;

&lt;p&gt;Logging is easy to start with but hard to master. In this post, we'll dive into how you can use your Rails application's logs to their full potential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging in Rails
&lt;/h2&gt;

&lt;p&gt;Let's talk about the basics first. When you start a new Rails application, logging is already set up for you. Rails will initialize a new instance of &lt;code&gt;ActiveSupport::Logger&lt;/code&gt; that you can use anywhere within your application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Rails.logger.debug&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Hello logs!'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

I, &lt;span class="o"&gt;[&lt;/span&gt;2019-01-04 05:14:33 &lt;span class="c"&gt;#123] INFO -- Main: Hello Logs!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Rails logger writes to the standard output or &lt;code&gt;log/&amp;lt;environment&amp;gt;.log&lt;/code&gt; and will automatically log incoming requests or executed queries in addition to any log messages you write explicitly.&lt;/p&gt;

&lt;p&gt;There are numerous ways in which you can configure Rails logs, as outlined excellently in the &lt;a href="https://guides.rubyonrails.org/debugging_rails_applications.html#the-logger"&gt;documentation for Rails&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To make the most of our logs, we are especially interested in setting log levels as well as log formatting.&lt;/p&gt;

&lt;p&gt;But before tackling that, let's talk about why we should write logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good Logging, Bad Logging
&lt;/h2&gt;

&lt;p&gt;The purpose of logs is to inform you about system events so you can react to them. For example, when an error happens, a log message should tell you about it in a way you can understand.&lt;/p&gt;

&lt;p&gt;How well you understand a log message depends on how descriptive and contextual it is. A descriptive log message provides relevant information about what happened. A contextual log message includes information about a system's state when the system wrote it.&lt;/p&gt;

&lt;p&gt;Let's consider a simple example to see why we need both. Here is some code that calls an external API and returns its response when a user performs a request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_external_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Method entered'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok?&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Response success'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Response failure'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ClientError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="no"&gt;An&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="n"&gt;occurred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's imagine a situation where a customer reports an issue, and you look into the logs for help. This is what you might see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:40:34.827604 &lt;span class="c"&gt;#100038]  INFO -- : [42f2e074-d53f-41e4-adca-13f59c6383ec] Processing by UserController#action as HTML&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:40:34.828084 &lt;span class="c"&gt;#100038]  INFO -- : [42f2e074-d53f-41e4-adca-13f59c6383ec] Method entered&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:40:34.933263 &lt;span class="c"&gt;#100038]  INFO -- : [42f2e074-d53f-41e4-adca-13f59c6383ec] Response success&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:40:34.933803 &lt;span class="c"&gt;#100038]  INFO -- : [42f2e074-d53f-41e4-adca-13f59c6383ec] Completed 200 OK in 106ms (Views: 0.2ms | Allocations: 2135)&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:40:35.959454 &lt;span class="c"&gt;#100038]  INFO -- : [458a209f-6b0e-4d29-a9b0-449d6ff3d29a] Started GET "/action" for 127.0.0.1 at 2022-07-10 10:40:35 +0200&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:40:35.959975 &lt;span class="c"&gt;#100038]  INFO -- : [458a209f-6b0e-4d29-a9b0-449d6ff3d29a] Processing by UserController#action as HTML&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:40:35.960081 &lt;span class="c"&gt;#100038]  INFO -- : [458a209f-6b0e-4d29-a9b0-449d6ff3d29a] Method entered&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:40:36.043971 &lt;span class="c"&gt;#100038]  INFO -- : [458a209f-6b0e-4d29-a9b0-449d6ff3d29a] Response failure&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:40:36.044316 &lt;span class="c"&gt;#100038]  INFO -- : [458a209f-6b0e-4d29-a9b0-449d6ff3d29a] Completed 200 OK in 84ms (Views: 0.1ms | Allocations: 1297)&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:42:49.701879 &lt;span class="c"&gt;#103892]  INFO -- : [e02c3a8f-c081-40aa-90fb-9d1474126403] Started GET "/action" for 127.0.0.1 at 2022-07-10 10:42:49 +0200&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:42:49.703400 &lt;span class="c"&gt;#103892]  INFO -- : [e02c3a8f-c081-40aa-90fb-9d1474126403] Processing by UserController#action as HTML&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:42:49.703745 &lt;span class="c"&gt;#103892]  INFO -- : [e02c3a8f-c081-40aa-90fb-9d1474126403] Method entered&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-10T10:42:49.775846 &lt;span class="c"&gt;#103892]  INFO -- : [e02c3a8f-c081-40aa-90fb-9d1474126403] An error occurred&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These log messages provide some information, but not nearly enough. We are no closer to finding out what's responsible for the error the customer has reported. These log messages lack descriptiveness and context. They are noise. How can we improve them?&lt;/p&gt;

&lt;h3&gt;
  
  
  Log Levels in Rails
&lt;/h3&gt;

&lt;p&gt;The default Rails logger provides the log levels &lt;code&gt;DEBUG&lt;/code&gt;, &lt;code&gt;INFO&lt;/code&gt;, &lt;code&gt;WARN&lt;/code&gt;, and &lt;code&gt;ERROR&lt;/code&gt;. Using them, you can group log messages into different categories of relevance. This not only helps you filter log messages, but also provides some context.&lt;/p&gt;

&lt;p&gt;It can be challenging to decide when to use which log level. Here are some rules of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DEBUG&lt;/code&gt;: Use this log level for detailed information about actions happening within the system. You may use debug statements when methods are entered or exited, or anywhere you think it may add value during debugging.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INFO&lt;/code&gt;: Whenever your system changes its state, or some relevant event occurs, reach for the &lt;code&gt;INFO&lt;/code&gt; level. Examples of common info messages in the context of Rails applications are received requests, requests sent to external APIs, or jobs starting and finishing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WARN&lt;/code&gt;: Use this log to signify that something unexpected has happened. It's not a problem yet (because your application can handle it), but if it keeps happening, it might warrant your attention.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ERROR&lt;/code&gt;: Use this log level when an error happens. Errors are invalid application states that you must resolve as soon as possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can change the kinds of log messages your application writes by modifying the respective environment file. Normally, Rails will discard &lt;code&gt;DEBUG&lt;/code&gt; messages in production, but you can change that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/production.rb&lt;/span&gt;
&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's apply the log-level suggestions above to our code sample.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_external_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Method entered'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok?&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Response success'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Response failure'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ClientError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'An error occurred'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When debugging, we can now identify root causes for issues by looking into &lt;code&gt;WARN&lt;/code&gt; and &lt;code&gt;ERROR&lt;/code&gt; messages. Sadly, our log messages haven't gotten any more descriptive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Descriptive Log Messages
&lt;/h3&gt;

&lt;p&gt;Descriptive log messages leave no room for interpretation. They provide the details necessary to give the reader instant knowledge about what happened.&lt;/p&gt;

&lt;p&gt;When you read messages such as &lt;code&gt;'An error occurred'&lt;/code&gt;, you are left wondering what the error is. You want to avoid such confusion when writing your log messages. The most important thing when writing descriptive log messages is to put yourself in the shoes of the log reader. Will they get all the information they need when reading your logs?&lt;/p&gt;

&lt;p&gt;Let's change the message &lt;code&gt;'An error occurred'&lt;/code&gt; to the error itself, including its message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's an improvement. Let's check the other log messages in our example. &lt;code&gt;'Method entered'&lt;/code&gt; doesn't tell us which method was called, &lt;code&gt;'Response success'&lt;/code&gt; leaves out the actual response, and &lt;code&gt;'Response failure'&lt;/code&gt; provides no information about the nature of the failure. Let's change that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_external_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Calling external api'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok?&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Request success, received &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Request returned &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ClientError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note&lt;/strong&gt;: You might have noticed we use the block syntax when performing string interpolation with our logs. Using it avoids unnecessary computation when the application's log level is higher than the log messages' level. For example, &lt;code&gt;log.debug("Some #{concatenation}")&lt;/code&gt; will always perform the string concatenation, but &lt;code&gt;log.debug { "Some #{concatenation}" }&lt;/code&gt; will only do so when the log level is set to debug.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:19:13.742108 &lt;span class="c"&gt;#122944]  INFO -- : [29d1ced1-3d05-4fbf-8b75-2a50e87ad749] Started GET "/users" for 127.0.0.1 at 2022-07-30 11:19:13 +0200&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:19:13.743058 &lt;span class="c"&gt;#122944]  INFO -- : [29d1ced1-3d05-4fbf-8b75-2a50e87ad749] Processing by UsersController#index as HTML&lt;/span&gt;
D, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:19:13.743248 &lt;span class="c"&gt;#122944] DEBUG -- : [29d1ced1-3d05-4fbf-8b75-2a50e87ad749] Calling external api, user 1, payload {:search=&amp;gt;"name"}&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:19:13.844355 &lt;span class="c"&gt;#122944]  INFO -- : [29d1ced1-3d05-4fbf-8b75-2a50e87ad749] Request completed. Received {"userId"=&amp;gt;1, "id"=&amp;gt;1, "title"=&amp;gt;"title", "completed"=&amp;gt;false}&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:19:13.844836 &lt;span class="c"&gt;#122944]  INFO -- : [29d1ced1-3d05-4fbf-8b75-2a50e87ad749] Completed 200 OK in 102ms (Views: 0.2ms | Allocations: 2195)&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:20:09.501016 &lt;span class="c"&gt;#123422]  INFO -- : [598eb218-d3c9-4e92-9236-13281cc660af] Started GET "/users" for 127.0.0.1 at 2022-07-30 11:20:09 +0200&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:20:09.502518 &lt;span class="c"&gt;#123422]  INFO -- : [598eb218-d3c9-4e92-9236-13281cc660af] Processing by UsersController#index as HTML&lt;/span&gt;
D, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:20:09.502848 &lt;span class="c"&gt;#123422] DEBUG -- : [598eb218-d3c9-4e92-9236-13281cc660af] Calling external api, user 1, payload {:search=&amp;gt;""}&lt;/span&gt;
W, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:20:09.574884 &lt;span class="c"&gt;#123422]  WARN -- : [598eb218-d3c9-4e92-9236-13281cc660af] Request failed with 400: Empty search&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:20:09.575415 &lt;span class="c"&gt;#123422]  INFO -- : [598eb218-d3c9-4e92-9236-13281cc660af] Completed 200 OK in 73ms (Views: 0.2ms | Allocations: 2106)&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:20:41.229494 &lt;span class="c"&gt;#125518]  INFO -- : [77b70a25-268c-43ea-a3da-8788086165f2] Started GET "/users" for 127.0.0.1 at 2022-07-30 11:20:41 +0200&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:20:41.230735 &lt;span class="c"&gt;#125518]  INFO -- : [77b70a25-268c-43ea-a3da-8788086165f2] Processing by UsersController#index as HTML&lt;/span&gt;
D, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:20:41.231039 &lt;span class="c"&gt;#125518] DEBUG -- : [77b70a25-268c-43ea-a3da-8788086165f2] Calling external api, user 1, payload {:search=&amp;gt;"long name"}&lt;/span&gt;
E, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:20:41.301237 &lt;span class="c"&gt;#125518] ERROR -- : [77b70a25-268c-43ea-a3da-8788086165f2] Request timeout exceeded&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-07-30T11:20:41.301878 &lt;span class="c"&gt;#125518]  INFO -- : [77b70a25-268c-43ea-a3da-8788086165f2] Completed 200 OK in 71ms (Views: 0.3ms | Allocations: 2085)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our logs read much better now. There is little doubt about what each log message signifies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Further Context for Log Messages
&lt;/h3&gt;

&lt;p&gt;Our new log messages provide a clear picture of what happened. Because this is a simple example, we also have a good understanding of why certain things happened. In reality, it's usually not that easy.&lt;/p&gt;

&lt;p&gt;Providing additional information about the context in which the system wrote each message can significantly help with debugging.&lt;/p&gt;

&lt;p&gt;For example, when logging requests or responses, it may be helpful to know who performed the request. Attaching a stack trace for errors might be useful so we can gather additional information about why the error occurred.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_external_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Calling external API. user_id: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, payload: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok?&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Request completed. Received &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parsed_response&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. user_id: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, payload: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Request failed with &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parsed_response&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. user_id: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, payload: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ClientError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;), &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backtrace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; }"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note&lt;/strong&gt;: You have likely noticed that creating log messages with contextual information can be cumbersome when you use Rails' default logger. Luckily, custom loggers such as &lt;a href="https://github.com/tilfin/ougai"&gt;Ougai&lt;/a&gt; or &lt;a href="https://github.com/hschne/mr-loga-loga"&gt;MrLogaLoga&lt;/a&gt; make this a lot easier.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured Logging
&lt;/h2&gt;

&lt;p&gt;Once you've made your logs readable for humans, you are ready to take things to the next level. You now need to make your logs readable by machines so you can use them for monitoring and data analysis.&lt;/p&gt;

&lt;p&gt;My go-to format is JSON, but depending on which tools you use, you may prefer other log formats such as &lt;a href="https://www.elastic.co/logstash/"&gt;Logstash&lt;/a&gt;. You can change the format of your log messages by creating a custom log formatter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/json_log_formatter.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JsonLogFormatter&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Formatter&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;progname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;time: &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;progname: &lt;/span&gt;&lt;span class="n"&gt;progname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;severity: &lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="n"&gt;msg2str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compact_blank&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# application.rb&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_formatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JsonLogFormatter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"time"&lt;/span&gt;:&lt;span class="s2"&gt;"2022-08-07T18:57:20.261+02:00"&lt;/span&gt;,&lt;span class="s2"&gt;"severity"&lt;/span&gt;:&lt;span class="s2"&gt;"INFO"&lt;/span&gt;,&lt;span class="s2"&gt;"message"&lt;/span&gt;:&lt;span class="s2"&gt;"Request completed"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rather than writing custom formatters yourself, you can use one of the many gems that provide custom log formatters. I recommend &lt;a href="https://github.com/roidrage/lograge"&gt;Lograge&lt;/a&gt;, which not only offers out-of-the-box formatting but also cleans up Rails' somewhat verbose request formatting so it's much easier to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rails Logging with AppSignal
&lt;/h2&gt;

&lt;p&gt;Now that your logs are nice and readable, it would be nice to make them more accessible. After all, you don't want to SSH into some machine and tail or grep the logs there, right?&lt;/p&gt;

&lt;p&gt;Luckily, AppSignal has recently launched a logging feature in beta! This allows you to inspect and analyze Rails logs directly in the AppSignal web interface.&lt;/p&gt;

&lt;p&gt;You can access our logging beta through the 'Logging' tab in the left-hand side menu:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qOqr7huO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2023-03/logging-menu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qOqr7huO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2023-03/logging-menu.png" alt="logs menu" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you follow the steps in our &lt;a href="https://docs.appsignal.com/logging/platforms/integrations/ruby.html"&gt;Ruby logging docs&lt;/a&gt;, you can configure the Rails Logger to use the &lt;code&gt;AppSignal::Logger&lt;/code&gt; class by placing the code below in the &lt;code&gt;config/environment.rb&lt;/code&gt; file before &lt;code&gt;initialization&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Use AppSignal's logger
Rails.logger = Appsignal::Logger.new("rails")

# Initialize the Rails application.
Rails.application.initialize!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also ingest structured logs in JSON format or with Lograge.&lt;/p&gt;

&lt;p&gt;Once you've set everything up, you should see your Rails logs show up on AppSignal!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vENTimHH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2023-03/logging-rails.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vENTimHH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2023-03/logging-rails.png" alt="logs" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging and Error Reporting
&lt;/h2&gt;

&lt;p&gt;You're reading this on the AppSignal blog, so you might wonder: why should I care about logging if I'm already using a nice error reporting platform?&lt;/p&gt;

&lt;p&gt;It's a good question. Error tracking tools provide detailed information about application errors. They capture the full stack trace of any application error and provide lots of contextual information out of the box.&lt;/p&gt;

&lt;p&gt;However, while most tools allow you to capture events other than errors, sending these tools an arbitrary amount of messages is generally infeasible. You won't be able to replace the detailed, historical information that well-written logs provide.&lt;/p&gt;

&lt;p&gt;In reality, well-written logs augment and support &lt;a href="https://www.appsignal.com/tour/errors"&gt;error tracking tools&lt;/a&gt;. You should likely use both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap Up
&lt;/h2&gt;

&lt;p&gt;In this post, we looked into how you can get the most out of your logs. We saw that getting started with logging in Rails is easy, but writing useful&lt;br&gt;
logs can be challenging.&lt;/p&gt;

&lt;p&gt;Logs that lack descriptiveness or context will end up just filling up your disk while providing little value. However, logs that use the correct log levels and provide readers with the information they need are a great asset.&lt;/p&gt;

&lt;p&gt;Once you've mastered writing great log messages, you can take things further and format them in a way that allows for log analysis and easy filtering.&lt;/p&gt;

&lt;p&gt;We also explored how you can use AppSignal's new logging feature to access your logs easily. Finally, we touched on how logs should augment error tracking tools rather than replace them.&lt;/p&gt;

&lt;p&gt;Happy logging!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>Add Feature Flags in Ruby on Rails with Flipper</title>
      <dc:creator>Hans Schnedlitz</dc:creator>
      <pubDate>Thu, 16 Jun 2022 11:18:58 +0000</pubDate>
      <link>https://dev.to/appsignal/add-feature-flags-in-ruby-on-rails-with-flipper-3cm4</link>
      <guid>https://dev.to/appsignal/add-feature-flags-in-ruby-on-rails-with-flipper-3cm4</guid>
      <description>&lt;p&gt;Picture this scenario: you are a Rails developer and have spent the last couple of days developing that awesome feature that everyone is waiting for. It's big and complex, but it went through rigorous testing, so you are confident everything works as it should. There are deadlines to meet, so you deploy. Immediately, all hell breaks loose.&lt;/p&gt;

&lt;p&gt;Your feature straight up breaks the entire app for some of your users. It's hard to say why. No bugs showed up during testing. You revert your change, but the damage has been done. Your customers aren't happy, and you will spend the foreseeable future doing damage control and investigating what went wrong.&lt;/p&gt;

&lt;p&gt;The problem is that releasing complex changes is inherently risky, no matter how much testing you do. It would have been nice if you had rolled this feature out gradually, maybe only to a handful of users first. It would have been even better if you could have switched the feature off as soon as the first problem showed up.&lt;/p&gt;

&lt;p&gt;This is precisely where feature flags come in. In this post, we'll dive into the ins and outs of feature flags in Rails, including how to set them up using the Flipper gem. But first: what are they?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Feature Flags in Ruby on Rails?
&lt;/h2&gt;

&lt;p&gt;Feature flags are a way to enable or disable a feature in your application while it is running. If a flag is enabled, the feature is used. Otherwise, it is not. Imagine something like this in pseudocode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;feature_enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:my_awesome_feature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Run awesome feature code&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="c1"&gt;# Not so awesome code&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name 'feature flag' is somewhat misleading. Controlling application behavior at runtime is not limited to features,&lt;br&gt;
but applies to any function within your program. You can use feature flags to try out performance tweaks and run A/B tests, for example.&lt;/p&gt;

&lt;p&gt;A significant difference between a feature flag and any other old conditional is you can modify feature flags at runtime.&lt;/p&gt;

&lt;p&gt;For illustration purposes, let's imagine a simple feature flagging mechanism using environment variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;feature_enabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;feature_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By modifying the value of the environment variable, we can switch parts of our application on or off at will. Which part of an application? Anything you want!&lt;/p&gt;

&lt;p&gt;You could hide parts of the UI if a feature is disabled. You could switch out parts of your code, which is very useful if you aren't confident in your latest refactor. One of my favorite uses of feature flags is to hide entire routes using &lt;a href="https://guides.rubyonrails.org/routing.html#advanced-constraints"&gt;routing constraints&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/constraint/feature_constraint.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeatureConstraint&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@feature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;matches?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;feature_enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@feature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'statistics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'statistics#index'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;constraints: &lt;/span&gt;&lt;span class="no"&gt;FeatureConstraint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:statistics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can already see that there are lots of applications for feature flags. Our naive implementation has one significant weakness, though. A feature is either on or off for every user.&lt;/p&gt;

&lt;p&gt;What if we want the ability to enable features only for specific users or groups of users, or only for some of the time? Let's take a look at Flipper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the Flipper Gem for Feature Flags
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/jnunemaker/flipper"&gt;Flipper&lt;/a&gt; is a gem that makes feature flags and different ways to toggle them&lt;br&gt;
available in Rails. It is highly modular. Apart from the main gem, you'll also have to pick a storage adapter — but more on that later. Let's use the ActiveRecord adapter for now.&lt;/p&gt;

&lt;p&gt;Add the following lines to your Gemfile and run &lt;code&gt;bundle install&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'flipper'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'flipper-active_record'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we use the ActiveRecord adapter, Flipper will store your feature flag in the database and thus requires setting up new database tables. Flipper comes with a handy generator that will create the necessary migrations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g flipper:active_record
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use Flipper in the same way as the naive implementation we used previously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:my_awesome_feature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Run awesome feature code but with Flipper!&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="c1"&gt;# Not so awesome code&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The big difference is that instead of modifying environment variables, you now get to toggle features using Flipper's user interface. Add the Flipper UI gem and install it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'flipper-ui'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modify your routes file to mount Flipper UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;mount&lt;/span&gt; &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/flipper'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you start your application and navigate to the Flipper UI, you will see an overview of all available features and their status. Go ahead and create a &lt;code&gt;my_awesome_feature&lt;/code&gt; feature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iZAy4xdG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2022-06/flipper-ui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iZAy4xdG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2022-06/flipper-ui.png" alt="The Flipper UI overview" width="745" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature Toggles in Flipper
&lt;/h2&gt;

&lt;p&gt;You've probably noticed that you can do much more than simply enable or disable a feature in Flipper UI. Flipper allows us to enable features for a subset of users or even for a percentage of the time. You can configure your feature flags using one of five different mechanisms.&lt;/p&gt;

&lt;p&gt;One of them is the boolean toggle, a global on-off switch. The other four are a lot more interesting than that: groups, actors, percentage of actors, and percentage of time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Groups
&lt;/h3&gt;

&lt;p&gt;To enable a feature only for a particular group of users, use the group toggle. If it doesn't exist, create the file &lt;code&gt;config/initializers/flipper.rb&lt;/code&gt; and add the following code to register a new group — for example, paid users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/flipper.rb&lt;/span&gt;
&lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:paid_users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond_to?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:paid_users?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paid?&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To use this new toggle, modify the Flipper invocation to pass the user — or actor, as the documentation likes to call them — and the feature name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:my_awesome_feature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Run awesome feature only if the user is paid&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="c1"&gt;# Not so awesome code&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flipper will use the block you defined in the initializer to decide if the feature is enabled for this user. In this case, it will invoke the &lt;code&gt;paid?&lt;/code&gt; method. You can now use this group in Flipper UI. Click the 'Add a Group' button and select your group.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--k1G8hol7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2022-06/flipper-ui-groups.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k1G8hol7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2022-06/flipper-ui-groups.png" alt="Adding a Flipper UI group feature" width="745" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your group doesn't have to be paid users. It could be members of your team or users who have signed up for a beta program. Using the group toggle is helpful if you want to release something to a subset of users. Imagine beta releases or internal releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Actors
&lt;/h3&gt;

&lt;p&gt;Sometimes, you might want to release something to a single individual. In this case, the actor toggle is something to consider.&lt;/p&gt;

&lt;p&gt;You don't have to change anything in your initializer to use this mechanism. However, you'll need to ensure that your actor model — which will be your user model most of the time — implements &lt;code&gt;flipper_id&lt;/code&gt;. Including &lt;code&gt;Flipper::Identifier&lt;/code&gt; takes care of that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# models/user.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationModel&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Identifier&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flipper_id&lt;/span&gt; &lt;span class="c1"&gt;# User;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then use this ID to enable a feature for individual users.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--12sFrYzI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2022-06/flipper-ui-actor.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--12sFrYzI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2022-06/flipper-ui-actor.png" alt="Adding a Flipper UI actor feature" width="745" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This feature toggle is handy if you want to preview functionality for particular individuals. Think about enabling specific test accounts for partners who need access to a feature before it's released to a broader audience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Percentage of Actors
&lt;/h3&gt;

&lt;p&gt;If you want to ramp up feature usage slowly, the percentage of actors toggle is for you. Imagine you've implemented some performance optimization, and you're not quite sure how it will behave in production. You would roll this out to a small percentage of users first and then slowly ramp up the usage while monitoring your application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vozZDKfv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2022-06/flipper-ui-percentage-of-actors.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vozZDKfv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://blog.appsignal.com/images/blog/2022-06/flipper-ui-percentage-of-actors.png" alt="Adding a Flipper UI percentage of actors feature" width="745" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If things go sideways, you can always reset the percentage to zero.&lt;/p&gt;

&lt;h3&gt;
  
  
  Percentage of Time
&lt;/h3&gt;

&lt;p&gt;Last but not least, you may enable a feature for a percentage of requests. You do not need to provide an actor when using this toggle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:my_awesome_feature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# Run awesome feature code only for a percentage of requests&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="c1"&gt;# Not so awesome code&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like to use this to compare the performance of two different implementations. Note that this feature toggle is random.&lt;br&gt;
The same user will experience different behavior on each request.&lt;/p&gt;
&lt;h2&gt;
  
  
  Advanced Configuration in Flipper
&lt;/h2&gt;

&lt;p&gt;We've already seen that we can use the ActiveRecord adapter to store feature flag information in our database. But depending on your specific needs, you might want to use &lt;a href="https://www.flippercloud.io/docs/adapters"&gt;a different adapter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the most part, all you need to do when swapping out storage adapters is to install the respective gem and configure Flipper accordingly. For example, if you want to use the Redis adapter, you add the &lt;code&gt;flipper-redis&lt;/code&gt; gem to your&lt;br&gt;
Gemfile and update the Flipper initializer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'flipper-redis'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/flipper.rb&lt;/span&gt;
&lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Adapters&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apart from configuring storage adapters, there are a lot of other aspects of Flipper that you can tweak.&lt;/p&gt;

&lt;p&gt;Adding feature flags incurs a slight performance penalty, so you usually want to add memoization and caching. You'll likely also want to restrict access to the Flipper UI to authorized users. These topics are out of scope for this post, but I recommend you check out the &lt;a href="https://www.flippercloud.io/docs/optimization"&gt;Flipper documentation&lt;/a&gt; if you want to dive deeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats to Feature Flags in Rails
&lt;/h2&gt;

&lt;p&gt;Feature flags are great. But as with so many things, there are tradeoffs to consider.&lt;/p&gt;

&lt;p&gt;Adding feature flags and maintaining them adds organizational overhead. Releasing and rolling out hidden features using flags is not as simple as deploying 'regular' code — you'll have to remember to turn it on, of course! That usually isn't too hard, but cleaning up feature flags that you don't need anymore might be.&lt;/p&gt;

&lt;p&gt;Every time you add a feature flag to your code, you add a conditional, and if you don't watch out, your code quickly becomes littered with them. Keeping track of when and why you created certain feature flags could be challenging. Sadly, Flipper UI itself does not offer much to manage feature flags.&lt;/p&gt;

&lt;p&gt;You should also be aware that using feature flags will incur a minor performance penalty. There are ways to mitigate this, but it can have a noticeable impact if you misconfigure or misuse Flipper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap Up: Get Started with Feature Flags in Ruby on Rails
&lt;/h2&gt;

&lt;p&gt;We looked at how feature flags can help you release with more confidence, learned how they function in principle, and saw how you can get started with feature flags using the Flipper gem.&lt;/p&gt;

&lt;p&gt;There are many situations where feature flags are handy. Depending on your use case, you might reach for one of the five different toggles — boolean, group, actor, percentage of actors, or percentage of requests.&lt;/p&gt;

&lt;p&gt;Although working with feature flags has its caveats and might take some getting used to, they are nevertheless a great tool to have at your disposal.&lt;/p&gt;

&lt;p&gt;Until next time, happy coding!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>Bootstrapping with Ruby on Rails Generators and Templates</title>
      <dc:creator>Hans Schnedlitz</dc:creator>
      <pubDate>Wed, 11 May 2022 12:00:46 +0000</pubDate>
      <link>https://dev.to/appsignal/bootstrapping-with-ruby-on-rails-generators-and-templates-34j5</link>
      <guid>https://dev.to/appsignal/bootstrapping-with-ruby-on-rails-generators-and-templates-34j5</guid>
      <description>&lt;p&gt;Rails' batteries-included approach is one of its greatest assets. No other framework makes it so effortless to get your application off the ground quickly, at least partially due to Rails' generators.&lt;/p&gt;

&lt;p&gt;If you've used Rails for any amount of time, you have come across generators. Need to create a new application? Run &lt;code&gt;rails new&lt;/code&gt;. Need to scaffold a bunch of new models and views? Run &lt;code&gt;rails generate scaffold&lt;/code&gt;. There are dozens more available to help you get started rapidly or streamline your workflow.&lt;/p&gt;

&lt;p&gt;But sometimes, using generators is just not enough. You might want to customize the behavior of these commands or even create your own. In this article, we'll take a closer look at generators - in particular, how to create your own custom Rails application using templates.&lt;/p&gt;

&lt;p&gt;Let's get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Rails Generators?
&lt;/h2&gt;

&lt;p&gt;Not to be confused with generator functions (which you might be familiar with from Python or Javascript), Rails generators are custom &lt;a href="https://github.com/rails/thor"&gt;Thor&lt;/a&gt; commands that focus on, well, generating things.&lt;/p&gt;

&lt;p&gt;There are lots of examples. You'll likely be familiar with the model generator (&lt;code&gt;rails generate model&lt;/code&gt;) for creating new ActiveRecord models or the migration generator (&lt;code&gt;rails generate migration&lt;/code&gt;) for generating new migrations. There is also &lt;code&gt;rails generate generator&lt;/code&gt; which — you guessed it — creates a new generator!&lt;/p&gt;

&lt;p&gt;Generators can call each other — for example, &lt;code&gt;rails scaffold&lt;/code&gt; will call numerous other generators — and provide methods to create or modify files, install gems, run specific rake tasks, and much more. Let's create a simple model spec generator to understand how this works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating Your Own Generator in Ruby on Rails
&lt;/h3&gt;

&lt;p&gt;Run the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;rails&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt; &lt;span class="n"&gt;generator&lt;/span&gt; &lt;span class="n"&gt;model_spec&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create several new files in &lt;code&gt;/lib/generators/model_spec&lt;/code&gt;. We can modify &lt;code&gt;model_spec_generator.rb&lt;/code&gt; in folder &lt;code&gt;lib/generators/model_spec/&lt;/code&gt; to create a model spec file in the correct directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ModelSpecGenerator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Generators&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NamedBase&lt;/span&gt;
  &lt;span class="n"&gt;source_root&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'templates'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__dir__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_model_spec&lt;/span&gt;
    &lt;span class="n"&gt;template_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'spec/models'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_spec.rb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="s1"&gt;'model_spec.rb.erb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template_file&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;template&lt;/code&gt; command will look for a template file in the &lt;code&gt;lib/generators/model_spec/templates&lt;/code&gt; directory and render it to the specified location — the &lt;code&gt;spec/models&lt;/code&gt; directory. The command will replace ERB-style variables found in the template file.&lt;/p&gt;

&lt;p&gt;By setting the &lt;code&gt;source_root&lt;/code&gt;, we let our generator know where it can find referenced template files. Template &lt;code&gt;model_spec.rb&lt;/code&gt; in folder &lt;code&gt;lib/generators/model_spec/templates/&lt;/code&gt; could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rails_helper'&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= class_name %&amp;gt;, :model
  pending "add some examples to (or delete) &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="kp"&gt;__FILE__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sx"&gt;"
end
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have created that file, you can run the generator to create a new spec file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails generate model_spec mymodel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many gems ship with generators such as this one. In fact, we created a simplified version of the generator &lt;a href="https://relishapp.com/rspec/rspec-rails/docs/generators"&gt;Rspec ships with&lt;/a&gt;. &lt;a href="https://github.com/thoughtbot/factory_bot_rails#generators"&gt;FactoryBot has a generator&lt;/a&gt; for factories. There are many more examples.&lt;/p&gt;

&lt;p&gt;The generators in various gems are more sophisticated than the one we created. We could make our generator take arguments or even hook it into existing generators such as &lt;code&gt;rails scaffold&lt;/code&gt;. Refer to the &lt;a href="https://guides.rubyonrails.org/generators.html#customizing-your-workflow"&gt;Rails generators documentation&lt;/a&gt; if you want to learn more.&lt;/p&gt;

&lt;p&gt;So generators have the potential to simplify your workflow in an existing application. But can we also use generators to customize setting up a new application?&lt;/p&gt;

&lt;p&gt;Enter templates!&lt;/p&gt;

&lt;h2&gt;
  
  
  Templates in Ruby on Rails
&lt;/h2&gt;

&lt;p&gt;As the name suggests, templates are files for customizing your application setup. Don't confuse these with the template files that we previously discussed!&lt;/p&gt;

&lt;p&gt;Under the hood, they are just generators with a specific purpose, as evidenced by the &lt;a href="https://guides.rubyonrails.org/rails_application_templates.html#template-api"&gt;template API&lt;/a&gt;. While not exactly identical to generators, they are very similar.&lt;/p&gt;

&lt;p&gt;If you have an existing template file, you can use it like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new myapp &lt;span class="nt"&gt;-m&lt;/span&gt; mytemplate.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rather than specifying a local file, you may also specify a URL. This is especially useful as it allows you to share application templates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new myapp &lt;span class="nt"&gt;-m&lt;/span&gt; https://gist.github.com/appsignal/12345/raw/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are not limited to using templates when running &lt;code&gt;rails new&lt;/code&gt; either. If you've already set up an app, you can apply templates afterward by executing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails app:template &lt;span class="nv"&gt;LOCATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://example.com/template.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Templates can be extremely useful. Who doesn't want to automate adding the same couple of gems and making the same configuration changes every time they create a new app? Creating your own application template is great fun — even if it doesn't save a lot of time in the long run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Your Own Template in Rails
&lt;/h2&gt;

&lt;p&gt;We now know about generators and how to use templates. Let's create a simple application template to automate some setup steps.&lt;/p&gt;

&lt;p&gt;How you set up your Rails app is very much down to personal preference, but here's an example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the &lt;a href="https://github.com/bkeepers/dotenv"&gt;dotenv&lt;/a&gt; gem.&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;.env.development&lt;/code&gt; file for the development environment.&lt;/li&gt;
&lt;li&gt;Adapt the database configuration file to use environment variables.&lt;/li&gt;
&lt;li&gt;Optionally &lt;a href="https://github.com/rspec/rspec-rails"&gt;install and set up Rspec&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's create a local file — &lt;code&gt;mytemplate.rb&lt;/code&gt; — and add &lt;code&gt;dotenv&lt;/code&gt; using the &lt;code&gt;gem&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'dotenv-rails'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;groups: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:development&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we've just added the dotenv gem, let's also create a &lt;code&gt;.env.development&lt;/code&gt; file to contain our database configuration.&lt;/p&gt;

&lt;p&gt;You can create a new file with specific content by using &lt;code&gt;create_file&lt;/code&gt;. You won't find this in the template or generator documentation, as the method is supplied by Thor. You might also come across the alias &lt;code&gt;file&lt;/code&gt;. Application templates are evaluated in the context of &lt;code&gt;Rails::Generators::AppGenerator&lt;/code&gt;, and that's exactly &lt;a href="https://github.com/rails/rails/blob/8f39fbe18a57ae74513edc8561c00a369fe10f08/railties/lib/rails/generators/rails/app/app_generator.rb#L522"&gt;where the &lt;code&gt;file&lt;/code&gt; alias is defined&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;create_file&lt;/span&gt; &lt;span class="s1"&gt;'.env.development'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;TXT&lt;/span&gt;&lt;span class="sh"&gt;
  DATABASE_HOST=localhost
  DATABASE_USERNAME=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;app_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
  DATABASE_PASSWORD=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;app_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;TXT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;app_name&lt;/code&gt; variable contains the first argument of &lt;code&gt;rails new&lt;/code&gt;. Using this variable, we can ensure our config file matches the generated application.&lt;/p&gt;

&lt;p&gt;Next, let's use our environment variables to connect to the database. We could overwrite the entire &lt;code&gt;config/database.yml&lt;/code&gt; using the &lt;code&gt;create_file&lt;/code&gt; command, but let's modify it instead using &lt;a href="https://guides.rubyonrails.org/generators.html#inject-into-file"&gt;&lt;code&gt;inject_into_file&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;inject_into_file&lt;/span&gt; &lt;span class="s1"&gt;'config/database.yml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;after: &lt;/span&gt;&lt;span class="sr"&gt;/database: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;app_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;_development\n/&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;RUBY&lt;/span&gt;&lt;span class="sh"&gt;
  host: &amp;lt;%= ENV['DATABASE_HOST'] %&amp;gt;
  username: &amp;lt;%= ENV['DATABASE_USERNAME'] %&amp;gt;
  password: &amp;lt;%= ENV['DATABASE_PASSWORD'] %&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;  RUBY&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can use both strings or regex with the &lt;code&gt;after&lt;/code&gt; argument to specify where to inject content.&lt;/p&gt;

&lt;p&gt;Of course, using this kind of configuration only makes sense if a user isn't creating an application with SQLite. You can check for the presence of certain arguments by using the &lt;code&gt;options&lt;/code&gt; variable. It's best you read the &lt;a href="https://github.com/rails/rails/blob/8f39fbe18a57ae74513edc8561c00a369fe10f08/railties/lib/rails/generators/database.rb#L14"&gt;source code of Rails' app generator&lt;/a&gt; to see which options are available.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:database&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'sqlite3'&lt;/span&gt;
  &lt;span class="c1"&gt;# Set up env vars and db configuration&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last but not least, let's also allow users to install Rspec if they want to. There are various methods for taking user input and creating interactive templates. The &lt;code&gt;yes?&lt;/code&gt; method asks a user for confirmation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;yes?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Would you like to install Rspec?'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rspec-rails'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;group: :test&lt;/span&gt;
  &lt;span class="n"&gt;after_bundle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt; &lt;span class="s1"&gt;'rspec:install'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We already know the &lt;code&gt;gem&lt;/code&gt; method, but &lt;code&gt;generate&lt;/code&gt; and &lt;code&gt;after_bundle&lt;/code&gt; are new.&lt;/p&gt;

&lt;p&gt;As mentioned before, Rspec adds its own generators, and you can call these generators (or any other ones, for that matter) directly from your template. But there is a catch — gems specified with the &lt;code&gt;gem&lt;/code&gt; method are only installed at the end of the template. Calling &lt;code&gt;generate&lt;/code&gt; with a generator supplied by such a gem would fail — which is why you should register the command as a callback with &lt;code&gt;after_bundle&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: Before we wrap up, a quick word about creating or modifying files. We used &lt;code&gt;create_file&lt;/code&gt; and &lt;code&gt;inject_into_file&lt;/code&gt;, but there are many other options. You may come across &lt;code&gt;copy_file&lt;/code&gt; or &lt;code&gt;template&lt;/code&gt; when reading different templates. I did not mention them here to keep things simple. If you want to create more advanced templates, you should know that these other methods for dealing with files exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result: The Final Template in Rails
&lt;/h2&gt;

&lt;p&gt;The final template should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Install dotenv&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'dotenv-rails'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;groups: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:development&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:database&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'sqlite3'&lt;/span&gt;
  &lt;span class="c1"&gt;# Set up .env.development&lt;/span&gt;
  &lt;span class="n"&gt;create_file&lt;/span&gt; &lt;span class="s1"&gt;'.env.development'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;TXT&lt;/span&gt;&lt;span class="sh"&gt;
    DATABASE_HOST=localhost
    DATABASE_USERNAME=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;app_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
    DATABASE_PASSWORD=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;app_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;  TXT&lt;/span&gt;

  &lt;span class="c1"&gt;# Modify database.yml&lt;/span&gt;
  &lt;span class="n"&gt;inject_into_file&lt;/span&gt; &lt;span class="s1"&gt;'config/database.yml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;after: &lt;/span&gt;&lt;span class="sr"&gt;/database: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;app_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;_development\n/&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;RUBY&lt;/span&gt;&lt;span class="sh"&gt;
  host: &amp;lt;%= ENV['DATABASE_HOST'] %&amp;gt;
  username: &amp;lt;%= ENV['DATABASE_USERNAME'] %&amp;gt;
  password: &amp;lt;%= ENV['DATABASE_PASSWORD'] %&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;  RUBY&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Optionally install Rspec&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;yes?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Would you like to install Rspec?'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rspec-rails'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;group: :test&lt;/span&gt;
  &lt;span class="n"&gt;after_bundle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt; &lt;span class="s1"&gt;'rspec:install'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can test this particular template by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new myapp &lt;span class="nt"&gt;-m&lt;/span&gt; mytemplate.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new myapp &lt;span class="nt"&gt;--database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql mytemplate.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To take advantage of the custom database configuration.&lt;/p&gt;

&lt;p&gt;As mentioned, this file is perfectly suited to share as a GitHub gist. Adapt it to suit your needs, upload it, and then share it with your colleagues, friends, and anyone else interested in your custom app template 😉.&lt;/p&gt;

&lt;h3&gt;
  
  
  Learn More About Rails Generators and Templates
&lt;/h3&gt;

&lt;p&gt;Needless to say, we've just scratched the surface here.&lt;/p&gt;

&lt;p&gt;Everyone has different preferences for writing and developing Rails apps, so there are numerous generators and application templates out there. You can learn a lot about doing specific customizations by reading about them. I recommend &lt;a href="https://github.com/excid3/jumpstart"&gt;Chris Oliver's Jumpstart&lt;/a&gt; and &lt;a href="https://railsbytes.com/"&gt;RailsBytes&lt;/a&gt;, the latter of which is a community-curated collection of templates.&lt;/p&gt;

&lt;p&gt;There is also &lt;a href="https://github.com/thoughtbot/suspenders"&gt;Thoughtbot's Suspenders&lt;/a&gt;, which inspired me to dig deeper into Rails generators and templates. I even wrote my own application template — &lt;a href="https://github.com/hschne/schienenzeppelin"&gt;Schienenzeppelin&lt;/a&gt; — which, while not up-to-date, might still provide some inspiration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap Up: Get Started with Ruby on Rails Generators and Templates
&lt;/h2&gt;

&lt;p&gt;In this post, we looked into the basics of Rails generators and how they are used. We created our own generators to simplify writing new model specs.&lt;/p&gt;

&lt;p&gt;We then delved into templates and learned how you could create a simple template to customize your application setup. This can be a bit of work, but also quite rewarding. If writing your own templates is not for you, there are many existing ones online to choose from!&lt;/p&gt;

&lt;p&gt;Happy templating!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>The Perils of Parallel Testing in Ruby on Rails</title>
      <dc:creator>Hans Schnedlitz</dc:creator>
      <pubDate>Wed, 30 Mar 2022 10:42:03 +0000</pubDate>
      <link>https://dev.to/appsignal/the-perils-of-parallel-testing-in-ruby-on-rails-abd</link>
      <guid>https://dev.to/appsignal/the-perils-of-parallel-testing-in-ruby-on-rails-abd</guid>
      <description>&lt;p&gt;Have you ever heard someone complain about their tests being too fast? Me neither.&lt;/p&gt;

&lt;p&gt;Fast tests mean fast feedback. Whether you run them locally or in a continuous integration pipeline, the earlier your tests finish, the earlier you can react to failures and improve your code. Besides the productivity gains, it is well known that slow tests make developers grumpy. Nobody likes their developers grumpy.&lt;/p&gt;

&lt;p&gt;With all that said, creating a lightning-fast test suite isn't always as easy as you'd hope. Luckily, Rails 6 introduced an exciting feature called &lt;strong&gt;parallel testing&lt;/strong&gt;. It is effortless to get started with and can speed up your tests by a lot. However, there are some pitfalls to watch out for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Parallel Testing?
&lt;/h2&gt;

&lt;p&gt;What does parallel testing even mean?&lt;/p&gt;

&lt;p&gt;When you run a test suite, your test runner will typically spawn a single process to execute your tests one after the other — or &lt;em&gt;serially&lt;/em&gt;. A single test process uses a single CPU core.&lt;/p&gt;

&lt;p&gt;As you can probably imagine, this approach doesn't take full advantage of modern hardware, which often sports dozens of CPU cores.&lt;/p&gt;

&lt;p&gt;You may have a fancy MacBook with 10 CPU cores, but sadly, that won't make your tests go any faster!&lt;/p&gt;

&lt;p&gt;We can change this by distributing individual tests to multiple worker processes. Tests will no longer run after each other, but next to each other — in &lt;em&gt;parallel&lt;/em&gt;. Running your test suite on two workers is twice as fast as running the same test suite on a single worker.&lt;/p&gt;

&lt;p&gt;The more cores your machine has, the more worker processes are feasible, and thus, the faster your test suite will finish. Say your tests usually take eight minutes to run. Running those same tests using four worker processes will take the runtime down to around two minutes!&lt;/p&gt;

&lt;p&gt;Imagine doing the same on a nice, fat 16 core machine, where we can spawn sixteen workers. Very nice indeed!&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Parallel Testing in Rails
&lt;/h2&gt;

&lt;p&gt;So how do we get there? Until recently, you could use third-party gems to parallelize your test suite, but starting from&lt;br&gt;
Rails 6, parallel tests come standard. It's as easy as adding &lt;code&gt;parallelize&lt;/code&gt; to your tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActiveSupport::TestCase&lt;/span&gt;
  &lt;span class="n"&gt;parallelize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;workers: :number_of_processors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using this configuration, Rails will automatically spawn worker processes based on the number of processors in your machine. Rails will also create namespaced databases (e.g. &lt;code&gt;database-test-0&lt;/code&gt;, &lt;code&gt;database-test-1&lt;/code&gt;, etc.) to run your tests against.&lt;/p&gt;

&lt;p&gt;That's all it takes to get started! Of course, there are some additional configuration options if you need them.&lt;/p&gt;

&lt;p&gt;Sometimes, you may have to perform a specific setup or cleanup for parallel tests. Rails provides two hooks for you to use — &lt;code&gt;parallelize_setup&lt;/code&gt; and &lt;code&gt;parallelize_teardown&lt;/code&gt;. These are called before and after new worker processes spawn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActiveSupport::TestCase&lt;/span&gt;
  &lt;span class="n"&gt;parallelize_setup&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="c1"&gt;# setup&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;parallelize_teardown&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="c1"&gt;# cleanup&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also manually set the number of workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActiveSupport::TestCase&lt;/span&gt;
  &lt;span class="n"&gt;parallelize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;workers: &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Use 4 worker processes&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, use the &lt;code&gt;PARALLEL_WORKERS&lt;/code&gt; environment variable to override an existing configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PARALLEL_WORKERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 rails &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also the option to use threads instead of workers to parallelize your test suite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActiveSupport::TestCase&lt;/span&gt;
  &lt;span class="n"&gt;parallelize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;workers: :number_of_processors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: :threads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;with: :threads&lt;/code&gt; is the default option when using JRuby or TruffleRuby. Using threads, in theory, provides slightly better performance. Threads require less overhead than processes, after all. In practice, however, I never found using threads all too useful, and you should be fine just sticking to process-based parallelization for the most part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beware the Pitfalls
&lt;/h2&gt;

&lt;p&gt;So all you need to do is add &lt;code&gt;parallelize&lt;/code&gt; to your existing tests to experience incredible speedup? It's that easy!&lt;/p&gt;

&lt;p&gt;If you are lucky, that really is true. It's more likely that you will hit some unexpected snags when first adding parallelization to your existing test suite. This certainly was the case for me!&lt;/p&gt;

&lt;p&gt;Let's get one thing out of the way. If you use RSpec rather than Minitest, you are out of luck. RSpec does not support Rails 6 built-in parallel testing. There is an &lt;a href="https://github.com/rspec/rspec-rails/issues/2104"&gt;ongoing discussion&lt;/a&gt; about changing that, but there hasn't been any significant progress for a while. If you want parallel tests with RSpec, your best bet is still using third-party gems such as &lt;a href="https://github.com/grosser/parallel_tests"&gt;grosser/parallel_tests&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Another unexpected issue that you might face is that running a small number of tests in parallel ends up being &lt;em&gt;slower&lt;/em&gt; then running them serially. Setting up parallel tests comes with a significant overhead — such as creating multiple databases — which can eliminate any gains you might get from parallelization.&lt;/p&gt;

&lt;p&gt;You might be better off disabling parallel tests for a small number of tests. You can do so by using the &lt;code&gt;PARALLEL_WORKERS&lt;/code&gt; environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PARALLEL_WORKERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 rails &lt;span class="nb"&gt;test test&lt;/span&gt;/controllers/my_controller_test.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rails 7 addressed this by enabling parallel execution only when you execute many tests. So if you've already upgraded, you won't experience this problem. Per default, the parallelization threshold is set to 50, but you can override it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_support&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test_parallelization_threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last problem I want to highlight is the one you are most likely to face, and it is also the most insidious and hardest to deal with. You may start seeing random failures when enabling parallel tests for your test suite. To understand how parallelization can cause this, let's look at a simple test case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FileTest&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;teardown&lt;/span&gt;
    &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test.txt'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s1"&gt;'create file'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test.txt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'created'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;assert_path_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test.txt'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s1"&gt;'delete file'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test.txt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'deleted'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test.txt'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;assert_not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test.txt'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Besides being a bit useless, this test is perfectly fine. It will pass 100% of the time as long as it's run serially. Each test is isolated, and executing these tests in random order does not cause them to fail. That changes when you add multiple processes or threads to the mix.&lt;/p&gt;

&lt;p&gt;When running tests in parallel, the execution order of individual statements in your tests can get changed up due to CPU scheduling. Looking at the example, you'll sometimes see execution orders such as this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Worker 1 executes&lt;/span&gt;
&lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test.txt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'created'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Worker 2 executes&lt;/span&gt;
&lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test.txt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'deleted'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test.txt'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;assert_not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test.txt'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Worker 1 executes&lt;/span&gt;
&lt;span class="n"&gt;assert_path_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test.txt'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since Worker 2 deleted the file before Worker 1's assertion was executed, the first test will fail — sometimes. To add insult to injury, a different execution order would sometimes make the second test fail and the first one pass.&lt;/p&gt;

&lt;p&gt;This simple example illustrates an issue that extends not only to files but to any singleton resource that your tests access in a non-thread-safe way. Suppose you write to a Redis database or an Elasticsearch index. In that case, you'll likely experience similar complications. What's worse, it may take you a while to uncover all tests that cause random failures — and even more time to fix all of them.&lt;/p&gt;

&lt;p&gt;There is no silver bullet to address flaky parallel tests. In general, you will need to ensure that multiple test processes do not share resources. For files, use &lt;a href="https://ruby-doc.org/stdlib-2.5.3/libdoc/tempfile/rdoc/Tempfile.html"&gt;Tempfiles&lt;/a&gt;. Use &lt;code&gt;parallelize_setup&lt;/code&gt; to create namespaced resources (e.g. Redis databases). And so on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Parallel Testing to Existing Rails Tests
&lt;/h2&gt;

&lt;p&gt;Let's say you struggle with random test failures due to parallelization and don't have the time to fix them. However, you still want to reap the benefits of parallel testing. You may prefer to enable it only for a subset of your tests.&lt;/p&gt;

&lt;p&gt;Only tests that call &lt;code&gt;parallelize&lt;/code&gt; will be parallelized after all, and by using concerns or parent classes, you can add parallel testing to your test suite one test class at a time. You could create a module like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Parallelize&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;included&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class_eval&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;parallelize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;workers: :number_of_processors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any test class that includes this module will now run in parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TestCase&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Parallelize&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, you could create a new test class like &lt;code&gt;ParallelTest&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ParallelTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TestCase&lt;/span&gt;
  &lt;span class="n"&gt;parallelize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;workers: :number_of_processors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, inherit from that for tests that should run in parallel — and leave out those that prove problematic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel Testing as a Bandaid
&lt;/h2&gt;

&lt;p&gt;Parallel testing provides impressive speed gains for little effort. Don't be fooled, though: it is no substitute for other approaches to improve your test suites' speed, but rather an addition.&lt;/p&gt;

&lt;p&gt;If you find your test suite is slow and can spare the effort, spend some time profiling it and addressing the root cause/s for the slowness. A slow test suite with parallel testing added to it will get faster, but never as fast as an already fast test suite that also runs in parallel!&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap Up
&lt;/h2&gt;

&lt;p&gt;In this post, we looked at what parallel testing is, how you can set it up and how to configure it. If you need a way to make your tests go faster, parallel testing provides just that.&lt;/p&gt;

&lt;p&gt;You might face some obstacles when adding parallel testing to your test suite. Don't be surprised when tests that ran stable for years suddenly start failing. You can work around them by parallelizing only a subset of your test suite.&lt;/p&gt;

&lt;p&gt;No matter which approach you choose, parallel testing is a fantastic tool to speed up your tests!&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
  </channel>
</rss>
