<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[96codes.dev | a dev blog by Benjamín Silva @silva96]]></title><description><![CDATA[A blog about software development, elixir, ruby, aws, javascript and best practices]]></description><link>https://blog.96codes.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1668794215982/qOMZwOurn.png</url><title>96codes.dev | a dev blog by Benjamín Silva @silva96</title><link>https://blog.96codes.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 12:03:04 GMT</lastBuildDate><atom:link href="https://blog.96codes.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What's stopping you from reading your Rails logs like this in 2025?]]></title><description><![CDATA[I’ve been building a small TUI to read Rails logs less painfully during development. It configures itself to produce and then parse JSON logs in real time, group SQL queries under their HTTP request, and gives you filters, colors, sorting and instant...]]></description><link>https://blog.96codes.dev/whats-stopping-you-from-reading-your-rails-logs-like-this-in-2025</link><guid isPermaLink="true">https://blog.96codes.dev/whats-stopping-you-from-reading-your-rails-logs-like-this-in-2025</guid><category><![CDATA[Logs]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Ruby]]></category><dc:creator><![CDATA[Benjamin Silva Herrera]]></dc:creator><pubDate>Mon, 10 Nov 2025 12:42:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762778424019/7d49f145-73f5-4951-ab14-1e24427bd33b.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve been building a small TUI to read Rails logs less painfully during development. It configures itself to produce and then parse JSON logs in real time, group SQL queries under their HTTP request, and gives you filters, colors, sorting and instant search.</p>
<p>It’s called <strong>LogBench</strong> and it’s free and open source, feel free to dig into <a target="_blank" href="https://github.com/silva96/log_bench">the repo</a> and put a star ⭐️ on it if you like it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762776821822/a46b3c8e-bba0-4cd8-bcb8-19fd8c1c41b4.webp" alt class="image--center mx-auto" /></p>
<h2 id="heading-installation">Installation</h2>
<p>Add it to your Gemfile, run <code>log_bench</code>, and watch your requests stream in.</p>
<pre><code class="lang-ruby">group <span class="hljs-symbol">:development</span> <span class="hljs-keyword">do</span>
  gem <span class="hljs-string">'log_bench'</span>
<span class="hljs-keyword">end</span>
</code></pre>
<pre><code class="lang-bash">bundle install
log_bench
</code></pre>
<p>That’s it. No config needed.  </p>
<p>If you want to use it in production just don’t put it inside the :development group and force enable it with an initializer:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># in config/initializers/log_bench.rb</span>
<span class="hljs-keyword">if</span> <span class="hljs-keyword">defined</span>?(LogBench)
  LogBench.setup <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
    config.enabled = <span class="hljs-literal">true</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-preview">Preview</h2>
<p>More screenshots from Omarchy themes below, and even more <a target="_blank" href="https://github.com/silva96/log_bench/issues/39">here</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762777779594/52e2a928-8d1f-4629-84dc-5e038176f4f3.webp" alt="Tokio Night theme" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762777840338/c707ad31-60a8-4ddf-8457-4556b21d2bc9.webp" alt class="image--center mx-auto" /></p>
<h2 id="heading-features">Features</h2>
<p>Here’s the full list of things it does:</p>
<ul>
<li><p>Real-time log reading with auto-scroll</p>
</li>
<li><p>Fast JSON log parsing</p>
</li>
<li><p>Split-view for request list and individual request details</p>
</li>
<li><p>Syntax highlighting and ANSI color support</p>
</li>
<li><p>Nice summary of the queries performed in a request.</p>
</li>
<li><p>Performance breakdown per request: duration, allocations, db time, view time</p>
</li>
<li><p>Sort by timestamp, duration, or status</p>
</li>
<li><p>Advanced filters: method, path, status, controller, action, request id</p>
</li>
<li><p>Free-text filter on the right pane to find specific SQL or log lines</p>
</li>
<li><p>Copy to clipboard:</p>
<ul>
<li><p>Left pane: full request summary</p>
</li>
<li><p>Right pane: selected SQL with call site</p>
</li>
</ul>
</li>
<li><p>Text selection mode to use the mouse for copy</p>
</li>
<li><p>Keyboard controls:</p>
<ul>
<li><p>Navigation: ↑ ↓ or j k</p>
</li>
<li><p>Pane switch: ← → or h l</p>
</li>
<li><p>Filter dialog: f</p>
</li>
<li><p>Clear filter: c</p>
</li>
<li><p>Sort: s</p>
</li>
<li><p>Auto-scroll toggle: a</p>
</li>
<li><p>Copy: y</p>
</li>
<li><p>Text selection toggle: t</p>
</li>
<li><p>Clear requests in memory: Ctrl+L</p>
</li>
<li><p>Undo clear: Ctrl+R</p>
</li>
<li><p>Quit: q</p>
</li>
</ul>
</li>
<li><p>Background job context:</p>
<ul>
<li><p>Colored job prefixes like [JobClass#job-id]</p>
</li>
<li><p>Works with ActiveJob when using the job logger</p>
</li>
<li><p>Works with plain Sidekiq via middleware</p>
</li>
<li><p>SQL queries inherit job context</p>
</li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Simple Ruby class to print ActiveRecord results as a table]]></title><description><![CDATA[You ever wanted to print a result of records in a fancier way?
 With this simple class  you can print tables like this:
records = Post.includes(:user).where(user_id: [1,2])
attributes = [:created_at, :title, {user: :name}]

TablePrinter.new(records, ...]]></description><link>https://blog.96codes.dev/simple-ruby-class-to-print-activerecord-results-as-a-table</link><guid isPermaLink="true">https://blog.96codes.dev/simple-ruby-class-to-print-activerecord-results-as-a-table</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><dc:creator><![CDATA[Benjamin Silva Herrera]]></dc:creator><pubDate>Fri, 05 Mar 2021 15:27:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1614957947561/cvBgJl3QC.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You ever wanted to print a result of records in a fancier way?</p>
<p> With this simple class  you can print tables like this:</p>
<pre><code class="lang-ruby">records = Post.includes(<span class="hljs-symbol">:user</span>).where(<span class="hljs-symbol">user_id:</span> [<span class="hljs-number">1</span>,<span class="hljs-number">2</span>])
attributes = [<span class="hljs-symbol">:created_at</span>, <span class="hljs-symbol">:title</span>, {<span class="hljs-symbol">user:</span> <span class="hljs-symbol">:name</span>}]

TablePrinter.new(records, attributes).print_table
</code></pre>
<pre><code class="lang-text">+-------------------------+-------------------+-----------------+
|  Created at             |  Title            |  User Name      |
+-------------------------+-------------------+-----------------+
| 2020-12-14 02:02:02 UTC | Un titulo 3       | Magdalena Silva |
| 2020-12-14 02:18:25 UTC | ¿Qué es escribir? | Benjamín Silva  |
| 2020-12-20 18:41:46 UTC | Primero para leo  | Magdalena Silva |
| 2021-02-06 14:18:54 UTC | A File            | Benjamín Silva  |
| 2020-12-20 04:09:37 UTC | aasdasd           | Benjamín Silva  |
| 2021-02-12 18:14:34 UTC |                   | Benjamín Silva  |
| 2020-12-14 01:50:20 UTC | Test super title  | Benjamín Silva  |
| 2021-02-06 03:32:02 UTC | lala5             | Benjamín Silva  |
+-------------------------+-------------------+-----------------+
</code></pre>
<p>This is an adaptation of this response, https://stackoverflow.com/a/28685559/1275069 to make it work with ActiveRecord right away. </p>
<p>It supports deeply nested attributes (example: <code>post.user.some_model.some_other.the_attribute</code>), but remember to include them in the query to avoid n+1.</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># frozen_string_literal: true</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TablePrinter</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(records, attrs)</span></span>
    @records = records
    @attrs = attrs
  <span class="hljs-keyword">end</span>

  <span class="hljs-comment"># rubocop:disable Rails/Output</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">print_table</span></span>
    buffer = [divider, header, divider, content, divider].join(<span class="hljs-string">"\n"</span>)
    puts buffer
  <span class="hljs-keyword">end</span>
  <span class="hljs-comment"># rubocop:enable Rails/Output</span>

  private

  <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:records</span>, <span class="hljs-symbol">:attrs</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">content</span></span>
    records.map { <span class="hljs-params">|row|</span> line(row) }
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">header</span></span>
    <span class="hljs-string">"| <span class="hljs-subst">#{columns.map { <span class="hljs-params">|_, g|</span> g[<span class="hljs-symbol">:label</span>].ljust(g[<span class="hljs-symbol">:width</span>]) }</span>.join(' | ')} |"</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">divider</span></span>
    <span class="hljs-string">"+-<span class="hljs-subst">#{columns.map { <span class="hljs-params">|_, g|</span> <span class="hljs-string">'-'</span> * g[<span class="hljs-symbol">:width</span>] }</span>.join('-+-')}-+"</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">line</span><span class="hljs-params">(row)</span></span>
    str = attrs.map { <span class="hljs-params">|attr|</span> row_value(row, attr).ljust(columns[attr][<span class="hljs-symbol">:width</span>]) }.join(<span class="hljs-string">' | '</span>)
    <span class="hljs-string">"| <span class="hljs-subst">#{str}</span> |"</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">row_value</span><span class="hljs-params">(row, attr)</span></span>
    <span class="hljs-keyword">if</span> attr.is_a?(Hash)
      row_value(row.send(attr.keys.first), attr.values.first)
    <span class="hljs-keyword">else</span>
      row ? row.send(attr).to_s : <span class="hljs-string">''</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">columns</span></span>
    @columns <span class="hljs-params">||</span>= col_labels.each_with_object({}) <span class="hljs-keyword">do</span> <span class="hljs-params">|(attr, label), h|</span>
      h[attr] = {
        <span class="hljs-symbol">label:</span> label,
        <span class="hljs-symbol">width:</span> [records.map { <span class="hljs-params">|row|</span> row_value(row, attr).to_s.size }.max, label.size].max
      }
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">col_labels</span></span>
    @col_labels <span class="hljs-params">||</span>= attrs.index_with { <span class="hljs-params">|attr|</span> nested_name(attr) }
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">nested_name</span><span class="hljs-params">(attr, memo = <span class="hljs-string">''</span>)</span></span>
    <span class="hljs-keyword">if</span> attr.is_a?(Hash)
      nested_name(attr.values.first, <span class="hljs-string">"<span class="hljs-subst">#{memo}</span> <span class="hljs-subst">#{attr.keys.first.to_s.humanize}</span>"</span>)
    <span class="hljs-keyword">else</span>
      <span class="hljs-string">"<span class="hljs-subst">#{memo}</span> <span class="hljs-subst">#{attr.to_s.humanize}</span>"</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Feel free to use it in your projects!
Happy Coding.</p>
]]></content:encoded></item><item><title><![CDATA[Finding the real ip: Cloudfront -> ELB -> Nginx -> Rails]]></title><description><![CDATA[There are several good reasons to put Cloudfront in front of your load balancers, as exposed  here
Performance

Cacheable Content.
Global Reach. 
Persistent Connections.
Collapsed Forwarding.

Security

Distributed Denial of Service (DDOS)
Encryption...]]></description><link>https://blog.96codes.dev/finding-the-real-ip-cloudfront-elb-nginx-rails</link><guid isPermaLink="true">https://blog.96codes.dev/finding-the-real-ip-cloudfront-elb-nginx-rails</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[AWS]]></category><category><![CDATA[nginx]]></category><dc:creator><![CDATA[Benjamin Silva Herrera]]></dc:creator><pubDate>Tue, 22 Dec 2020 19:48:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1608667497225/v_OZU5EiU.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There are several good reasons to put Cloudfront in front of your load balancers, as exposed  <a target="_blank" href="https://aws.amazon.com/es/blogs/networking-and-content-delivery/dynamic-whole-site-delivery-with-amazon-cloudfront/">here</a></p>
<p><strong>Performance</strong></p>
<ul>
<li>Cacheable Content.</li>
<li>Global Reach. </li>
<li>Persistent Connections.</li>
<li>Collapsed Forwarding.</li>
</ul>
<p><strong>Security</strong></p>
<ul>
<li>Distributed Denial of Service (DDOS)</li>
<li>Encryption in Transit</li>
<li>Web Application Firewall (WAF).</li>
</ul>
<p>Keep in mind there is an additional fee for using Cloudfront, but the traffic inside the aws network has no costs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1608663206482/5eh2gFm4g.png" alt="image.png" /></p>
<p>Then, of course we need to load balance our EC2 instances for high availability.
Then the requests are handled by nginx and passed via proxy_pass to Puma (rails server).</p>
<p>That's a lot of jumps to serve a request!</p>
<p>I wanted to log rails requests with the real user IP address, but with all these jumps, it's not a trivial thing.</p>
<p>What you need to do is forward to the upstream the X-Forwarded-For header, which comes all the way up from Cloudfront, passing through ELB to finally be passed to Nginx</p>
<pre><code class="lang-nginx"><span class="hljs-attribute">upstream</span> your_app {
  <span class="hljs-attribute">server</span> unix:/home/ubuntu/www/shared/tmp/sockets/puma.sock fail_timeout=<span class="hljs-number">0</span>;
}
<span class="hljs-section">server</span> {
    <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span> default_server;
    <span class="hljs-attribute">server_name</span> yourapp.com;
    <span class="hljs-attribute">gzip</span> <span class="hljs-literal">on</span>;

     <span class="hljs-attribute">location</span> / {
        <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto https;
        <span class="hljs-attribute">proxy_set_header</span> X-Real-IP <span class="hljs-variable">$remote_addr</span>;
        <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>; <span class="hljs-comment"># &lt;- this header !</span>
        <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$http_host</span>;
        <span class="hljs-attribute">proxy_redirect</span> <span class="hljs-literal">off</span>;
        <span class="hljs-attribute">proxy_pass</span> http://your_app;
    }
}
</code></pre>
<p>That header comes in the form of a comma separated string, so for every jump, an ip is appended. For this reason, the first ip "might be" the real user ip. (unless someone manually injects that header in the request)</p>
<p>Having this, we can now use it in Rails. I changed this in production.rb</p>
<pre><code class="lang-diff"><span class="hljs-deletion">-  config.log_tags = %i[request_id remote_ip]</span>
<span class="hljs-addition">+  config.log_tags = [:request_id, -&gt;(request) { request.forwarded_for&amp;.first || request.remote_ip}]</span>
</code></pre>
<p>The interesting part is adding this lambda to the log_tags</p>
<pre><code class="lang-ruby">-&gt;(request) { request.forwarded_for&amp;.first <span class="hljs-params">||</span> request.remote_ip }
</code></pre>
<p>Where <code>forwarded_for</code> is defined in the Rack request class:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">forwarded_for</span></span>
  <span class="hljs-keyword">if</span> value = get_header(HTTP_X_FORWARDED_FOR)
    split_header(value).map <span class="hljs-keyword">do</span> <span class="hljs-params">|authority|</span>
      split_authority(wrap_ipv6(authority))[<span class="hljs-number">1</span>]
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>We try to find the first ip in the array, which as mentioned earlier, would be the first jump (real user ip). We use safe navigator in case the header is not present, and we fallback to remote_ip in case we could not find a forwarded ip.</p>
<pre><code>I, [<span class="hljs-number">2020</span><span class="hljs-number">-12</span><span class="hljs-number">-22</span>T19:<span class="hljs-number">46</span>:<span class="hljs-number">31.273143</span> #<span class="hljs-number">39407</span>]  <span class="hljs-keyword">INFO</span> <span class="hljs-comment">-- : [dfbe8700-908a-4405-bb0c-a23ac5359a5f] [190.8.117.22] Started GET "/" for 64.252.68.65 at 2020-12-22 19:46:31 +0000</span>
</code></pre><p>the first ip in this log is the real ip of the user (190.8.117.22), while the last ip (64.252.68.65) is the IP of the Cloudfront edge location.</p>
]]></content:encoded></item><item><title><![CDATA[Elixir Phoenix development using Docker and Docker-Compose]]></title><description><![CDATA[Quick and dirty, this is my recipe for local development of elixir phoenix apps using docker and docker-compose.
3 important files:

Dockerfile
docker-compose.yml
dev.exs

Dockerfile
FROM bitwalker/alpine-elixir-phoenix:latest

# Cache elixir deps
AD...]]></description><link>https://blog.96codes.dev/elixir-phoenix-development-using-docker-and-docker-compose</link><guid isPermaLink="true">https://blog.96codes.dev/elixir-phoenix-development-using-docker-and-docker-compose</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Benjamin Silva Herrera]]></dc:creator><pubDate>Thu, 26 Nov 2020 01:06:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1606352754161/WUs2Ocy5y.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Quick and dirty, this is my recipe for local development of elixir phoenix apps using docker and docker-compose.</p>
<p>3 important files:</p>
<ul>
<li>Dockerfile</li>
<li>docker-compose.yml</li>
<li>dev.exs</li>
</ul>
<h3 id="dockerfile">Dockerfile</h3>
<pre><code class="lang-docker">FROM bitwalker/alpine-elixir-phoenix:latest

# Cache elixir deps
ADD mix.exs mix.lock ./
RUN mix do deps.get, deps.compile

# Same with npm deps
ADD assets/package.json assets/
RUN cd assets &amp;&amp; npm install

CMD ["mix", "phx.server"]
</code></pre>
<h3 id="docker-composeyml">docker-compose.yml</h3>
<pre><code class="lang-yml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3.7'</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">web:</span>
    <span class="hljs-attr">build:</span> <span class="hljs-string">'.'</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"4000:4000"</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/opt/app/assets/node_modules</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/opt/app/deps</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">.:/opt/app</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">db</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">MIX_ENV:</span> <span class="hljs-string">'dev'</span>
  <span class="hljs-attr">db:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:alpine</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-number">5432</span><span class="hljs-string">:5432</span>
</code></pre>
<p>here we mount 3 volumes, the first two hold the deps so we don't recompile elixir deps or install npm packages every time we build or run the service. The last volume is our app itself, this way, changes reflect immediately without rebuilding or restarting the service.</p>
<h3 id="devexs">dev.exs</h3>
<pre><code class="lang-elixir">config :yourapp, Yourapp.Repo,
  username: "postgres",
  password: "postgres",
  database: "yourapp_dev",
  hostname: "db",
  show_sensitive_data_on_connection_error: true,
  pool_size: 10
</code></pre>
<p>Just make sure to override the hostname with <code>db</code> and use the default user and password for postgres.</p>
<p>after this you can do <code>docker-compose build</code> and the <code>docker-compose up</code> and head to <a target="_blank" href="http://localhost:4000">http://localhost:4000</a> in your browser.</p>
<p>if you want to enter to the container that is running:</p>
<p><code>docker container ls -a</code></p>
<p>Find the one with the _web suffix, mine is <code>myapp_web</code></p>
<p>A one liner to enter to it would be something like this:</p>
<p><code>docker exec -it $(docker ps | grep 'myapp_web' | awk '{print $1}') /bin/bash</code></p>
]]></content:encoded></item><item><title><![CDATA[Continuous Integration in Elixir Phoenix with Travis-CI and Codecov]]></title><description><![CDATA[I'll show how to get travis-ci run your test suit and then upload the coverage report to codecov. All this, being shown on github pull request checks for reference.
Something I've been doing more extensively lately is Unit Testing, not necessarily do...]]></description><link>https://blog.96codes.dev/continuous-integration-in-elixir-phoenix-with-travis-ci-and-codecov</link><guid isPermaLink="true">https://blog.96codes.dev/continuous-integration-in-elixir-phoenix-with-travis-ci-and-codecov</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Continuous Integration]]></category><dc:creator><![CDATA[Benjamin Silva Herrera]]></dc:creator><pubDate>Thu, 26 Nov 2020 00:36:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1606350982616/bHxQpRuN4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'll show how to get travis-ci run your test suit and then upload the coverage report to codecov. All this, being shown on github pull request checks for reference.</p>
<p>Something I've been doing more extensively lately is Unit Testing, not necessarily doing TDD but Unit testing per se. When you start enjoying it it becomes like a challenge and also an addiction, getting you coverage reporting tool show a 100% coverage badge in your project is a big joy. Unit Testing also makes you produce better code and you feel safer, so it's something positive to add to your development process.</p>
<h3 id="hands-on">Hands on</h3>
<p>First, you will have to register at <a target="_blank" href="https://travis-ci.org">travis-ci.org</a> and <a target="_blank" href="https://codecov.io">codecov.io</a> both have free plans for public repositories. (sign up using github)</p>
<p>Select the repositories you want to integrate in both services like shown above:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1606349881982/zGdGgZrsl.png" alt="Travis select repositories" /></p>
<p>For the reporting tool we will be using the package <code>ExCoveralls</code>, add the following to your <code>mix.exs</code> file in your Phoenix project</p>
<pre><code class="lang-elixir">defmodule Yourapp.MixProject do
  use Mix.Project

  def project do
    [
      app: :yourapp,
      version: "0.1.0",
      elixir: "~&gt; 1.5",
      elixirc_paths: elixirc_paths(Mix.env()),
      compilers: [:phoenix, :gettext] ++ Mix.compilers(),
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps(),
      test_coverage: [tool: ExCoveralls] # ADD THIS LINE
    ]
  end

  ...

  defp deps do
    [
      {:phoenix, "~&gt; 1.4.9"},
      {:phoenix_pubsub, "~&gt; 1.1"},
      {:phoenix_ecto, "~&gt; 4.0"},
      {:ecto_sql, "~&gt; 3.1"},
      {:postgrex, "&gt;= 0.0.0"},
      {:phoenix_html, "~&gt; 2.11"},
      {:excoveralls, "~&gt; 0.11.1"}, # ADD THIS LINE
      {:phoenix_live_reload, "~&gt; 1.2", only: :dev},
      {:gettext, "~&gt; 0.11"},
      {:jason, "~&gt; 1.0"},
      {:plug_cowboy, "~&gt; 2.0"}
    ]
  end

  ...

end
</code></pre>
<p>Also create a <code>.travis.yml</code> file in the root folder of your project, as follows:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">language:</span> <span class="hljs-string">elixir</span>

<span class="hljs-attr">elixir:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">'1.9.1'</span>
<span class="hljs-attr">otp_release:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">'22.0.7'</span>

<span class="hljs-attr">env:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">MIX_ENV=test</span>

<span class="hljs-attr">addons:</span>
  <span class="hljs-attr">postgresql:</span> <span class="hljs-string">'9.6'</span>

<span class="hljs-attr">services:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">postgresql</span>

<span class="hljs-attr">before_script:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">cp</span> <span class="hljs-string">config/travis.exs</span> <span class="hljs-string">config/test.exs</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">mix</span> <span class="hljs-string">do</span> <span class="hljs-string">ecto.create,</span> <span class="hljs-string">ecto.migrate</span>

<span class="hljs-attr">script:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">mix</span> <span class="hljs-string">do</span> <span class="hljs-string">compile</span> <span class="hljs-string">--warnings-as-errors,</span> <span class="hljs-string">coveralls.json</span>

<span class="hljs-attr">after_success:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">bash</span> <span class="hljs-string">&lt;(curl</span> <span class="hljs-string">-s</span> <span class="hljs-string">https://codecov.io/bash)</span>

<span class="hljs-attr">cache:</span>
  <span class="hljs-attr">directories:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">_build</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">deps</span>
</code></pre>
<p>The before_script will replace a <code>test.exs</code> file for the <code>travis.exs</code> so it will run an environment with specific things for travis to work properly.</p>
<p>So we need to create this <code>travis.exs</code> under <code>config</code> folder</p>
<pre><code class="lang-elixir">use Mix.Config

# Configure your database
config :yourapp, yourapp.Repo,
  username: "postgres",
  password: "",
  database: "yourapp_test",
  hostname: "localhost",
  pool: Ecto.Adapters.SQL.Sandbox

# We don't run a server during test. If one is required,
# you can enable the server option below.
config :yourapp, yourappWeb.Endpoint,
  http: [port: 4002],
  server: false

# Print only warnings and errors during test
config :logger, level: :warn
</code></pre>
<p>You will also want to configure the coverage reporting tool to exclude some files we are not going to test nor take in account for our coverage metrics</p>
<p>create a <code>coveralls.json</code> file in the root folder of the project</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"coverage_options"</span>: {
    <span class="hljs-attr">"minimum_coverage"</span>: <span class="hljs-number">100</span>
  },
  <span class="hljs-attr">"skip_files"</span>: [
    <span class="hljs-string">"test/"</span>,
    <span class="hljs-string">"lib/yourapp_web.ex"</span>,
    <span class="hljs-string">"lib/yourapp.ex"</span>,
    <span class="hljs-string">"lib/yourapp/application.ex"</span>,
    <span class="hljs-string">"lib/yourapp/repo.ex"</span>,
    <span class="hljs-string">"lib/yourapp_web/endpoint.ex"</span>,
    <span class="hljs-string">"lib/yourapp_web/gettext.ex"</span>,
    <span class="hljs-string">"lib/yourapp_web/views/error_helpers.ex"</span>
  ]
}
</code></pre>
<p>This minimum_coverage config will mark as fail a build that doesn't have 100% coverage, which is hard to achieve in a real life project (but not impossible!).</p>
<p>to show badges of the build passing and coverage percentage like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1606349962455/QVwtyccC_.png" alt="Github Badges" /></p>
<p>add this, replacing yourgithub with your github username and yourrepo with the repository name</p>
<pre><code class="lang-markdown">[<span class="hljs-string">![codecov</span>](<span class="hljs-link">https://codecov.io/gh/yourgithub/yourrepo/branch/master/graph/badge.svg</span>)](<span class="hljs-link">https://codecov.io/gh/yourgithub/yourrepo</span>)
[<span class="hljs-string">![Build Status</span>](<span class="hljs-link">https://travis-ci.org/yourgithub/yourrepo.svg?branch=master</span>)](<span class="hljs-link">https://travis-ci.org/yourgithub/yourrepo</span>)
</code></pre>
<p>With all this configuration, every time you create a pull request you will see something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1606350027531/rFVxaLPBV.png" alt="Github PR coverage comment1" />
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1606350067886/8zj7VUlko.png" alt="Github PR coverage comment2" />
and the PR list will show checks or crosses depending on the check status</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1606350096497/lDrz75P9Z.png" alt="Github PR checks with travis-ci" /></p>
]]></content:encoded></item><item><title><![CDATA[Install Elixir and Erlang with asdf version manager]]></title><description><![CDATA[If you come from Ruby on Rails for example, it's very common to have rvm to manage your Ruby versions, but also you would probably need to install nvm to manage versions for Node.js too (since Rails now has webpack  assets pipeline).
This same story ...]]></description><link>https://blog.96codes.dev/install-elixir-and-erlang-with-asdf-version-manager</link><guid isPermaLink="true">https://blog.96codes.dev/install-elixir-and-erlang-with-asdf-version-manager</guid><category><![CDATA[dev tools]]></category><category><![CDATA[Elixir]]></category><category><![CDATA[Erlang]]></category><dc:creator><![CDATA[Benjamin Silva Herrera]]></dc:creator><pubDate>Wed, 25 Nov 2020 23:00:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1606347720062/rYg4mPksS.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you come from <em><code>Ruby on Rails</code></em> for example, it's very common to have <em><code>rvm</code></em> to manage your <em><code>Ruby</code></em> versions, but also you would probably need to install <em><code>nvm</code></em> to manage versions for <em><code>Node.js</code></em> too (since <em><code>Rails</code></em> now has <em><code>webpack</code></em>  assets pipeline).</p>
<p>This same story happens with <em><code>Phoenix Framework</code></em>, but even adding a third language to the stage: <em><code>Erlang</code></em>, <em><code>Elixir</code></em> and <em><code>Node.js</code></em></p>
<p>So how do we manage all involved languages versions with one single version manager?</p>
<blockquote>
<p>meet <code>asdf</code> version manager</p>
</blockquote>
<p>You can install it following the <a target="_blank" href="https://asdf-vm.com/#/core-manage-asdf-vm?id=install-asdf-vm">installation instructions</a></p>
<p>After installing, simply install an Erlang version by running</p>
<pre><code class="lang-text"># to add the erlang plugin
$ asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git
# to list all available erlang versions
$ asdf list-all erlang
</code></pre>
<p>A long list may appear, you usually would want to install the latest (at the bottom)</p>
<pre><code class="lang-text">$ asdf install erlang 22.0.7
</code></pre>
<p>After installing Erlang, you can proceed to install Elixir with the same approach</p>
<pre><code class="lang-text">$ asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
$ asdf list-all elixir
$ asdf install elixir 1.4.9
</code></pre>
<p>Then, in your project folder, you can tell asdf to select specific versions of software by adding a <code>.tool-versions</code> file with some content like this:</p>
<pre><code><span class="hljs-attribute">elixir</span> <span class="hljs-number">1</span>.<span class="hljs-number">9</span>.<span class="hljs-number">1</span>
<span class="hljs-attribute">erlang</span> <span class="hljs-number">22</span>.<span class="hljs-number">0</span>.<span class="hljs-number">7</span>
<span class="hljs-attribute">nodejs</span> <span class="hljs-number">10</span>.<span class="hljs-number">16</span>.<span class="hljs-number">0</span>
</code></pre><p>If you call <code>asdf current</code> to see whats the current versions of each configured plugin inside the current working directory</p>
<pre><code class="lang-text">$ asdf current

  elixir   1.9.1    (set by /Users/benja/dev/newslettex/.tool-versions)
  erlang   22.0.7   (set by /Users/benja/dev/newslettex/.tool-versions)
  version 10.16.0 is not installed for nodejs
</code></pre>
<p>You notice that it says node 10.16.0 is not installed, because it reads the <code>.tool-versions</code> and finds out we don't have the requested nodejs version. To install it, simply:</p>
<pre><code class="lang-text">$ asdf plugin-add nodejs https://github.com/asdf-vm/asdf-nodejs.git
$ brew install coreutils
$ brew install gpg
$ bash ~/.asdf/plugins/nodejs/bin/import-release-team-keyring
$ asdf install nodejs 10.16.0
</code></pre>
<p>Now you have all your versions managed under asdf.</p>
]]></content:encoded></item></channel></rss>