<?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>Forem: Mariusz Hausenplas</title>
    <description>The latest articles on Forem by Mariusz Hausenplas (@xlts).</description>
    <link>https://forem.com/xlts</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%2F371206%2F1f2b5348-e934-4d76-8b93-d4393dd83e54.jpeg</url>
      <title>Forem: Mariusz Hausenplas</title>
      <link>https://forem.com/xlts</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/xlts"/>
    <language>en</language>
    <item>
      <title>Automated on-demand deployments with Semaphore CI Classic API (and Ruby)</title>
      <dc:creator>Mariusz Hausenplas</dc:creator>
      <pubDate>Mon, 11 May 2020 18:02:27 +0000</pubDate>
      <link>https://forem.com/xlts/automated-on-demand-deployments-with-semaphore-ci-classic-api-and-ruby-dpo</link>
      <guid>https://forem.com/xlts/automated-on-demand-deployments-with-semaphore-ci-classic-api-and-ruby-dpo</guid>
      <description>&lt;p&gt;Semaphore is a popular tool used to create CI/CD pipelines. It does a great job at quickly integrating with Git repositories and running build or deployment tasks. Additionally, it comes with two different flavours: the standard Semaphore Classic and, the much more sophisticated Semaphore 2.0.&lt;/p&gt;

&lt;p&gt;While I'm not fully familiar with Semaphore 2.0 capabilities, one thing I can tell about Semaphore Classic is that it feels like one of the good, old Unix programs that was designed to do one simple thing, and do it well. Or, perhaps more accurately, it feels like a tool that was built with the Convention over Configuration rule in mind: it can definitely do a lot of things, but only in a specific way, and under specific assumptions.&lt;/p&gt;

&lt;p&gt;In this post I'll explore the surprisingly underdocumented capabilities of Semaphore Classic HTTP API to create a custom workflow supporting on-demand deployments to different servers.&lt;/p&gt;

&lt;p&gt;Now, imagine you've submitted a pull request and you'd like to test it somehow, e.g. on a staging environment. At this point you've probably already defined a new staging server that Semaphore could deploy to. So, all you have to do is to select a branch, pick a build and click &lt;code&gt;Deploy manually&lt;/code&gt; and select your non-production server. Pretty easy, eh? Well, what if you misclicked and accidentally deployed to production? Of course you could count on your reflexes and instantly stop the deploy, but we all know that it's better to automate such risky processes. Human error is inevitable. So where do we start?&lt;/p&gt;

&lt;p&gt;Semaphore CI operates on a small number of easy-to-comprehend concepts: Branches, Builds, Servers and Deploys. As we saw in the previous paragraph, a build is triggered by some kind of an external event (e.g. by a new brach being created or a new pull request being submitted). Then, you'd usually want CI to deploy this build to a specific server. These concepts map to entities in the HTTP API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://semaphoreci.com/docs/branches-and-builds-api.html#branch_status"&gt;&lt;code&gt;Branch and Build&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://semaphoreci.com/docs/servers-and-deploys-api.html#server_status"&gt;&lt;code&gt;Server&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://semaphoreci.com/docs/servers-and-deploys-api.html#deploy_information"&gt;&lt;code&gt;Deploy&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this knowledge in mind, let's try to automate something!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;URL structure &amp;amp; basic Semaphore API client setup&lt;/p&gt;

&lt;p&gt;In this preliminary step, let's create a simple Ruby class that uses   the &lt;a href="https://github.com/jnunemaker/httparty"&gt;&lt;code&gt;HTTParty&lt;/code&gt;&lt;/a&gt; gem and lists required configuration variables that'll be passed in requests to Semaphore API.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SemaphoreClient&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;

  &lt;span class="no"&gt;PROJECT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SEMAPHORE_PROJECT_ID"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="no"&gt;SERVER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SEMAPHORE_SERVER_ID"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;base_uri&lt;/span&gt; &lt;span class="s2"&gt;"https://semaphoreci.com/api/v1/projects/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="n"&gt;default_params&lt;/span&gt; &lt;span class="ss"&gt;auth_token: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SEMAPHORE_API_AUTH_TOKEN"&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;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;base_uri&lt;/code&gt; method defines the root URL of Semaphore Classic API&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auth_token&lt;/code&gt; param will be passed in every request. This is the User   Authentication Token found in &lt;a href="https://semaphoreci.com/users/edit"&gt;Account Settings&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PROJECT_ID&lt;/code&gt; is an ID of a Semaphore project, corresponding to a Git repository. This is a bit tricky to find: you can either query the &lt;a href="https://semaphoreci.com/docs/projects-api.html#users-projects"&gt;Projects API&lt;/a&gt; to find your repo, or go to Project Badge Settings where you can copy-paste the ID from provided snippets. This should look something like this: &lt;code&gt;https://semaphoreci.com/api/v1/projects/&amp;lt;project_id&amp;gt;/badge.svg&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SERVER_ID&lt;/code&gt; is an ID of a target deployment server, e.g. the one you'd like to use to deploy to your Staging/Preview environment. It seems that the only way to fetch internal IDs of servers is via the &lt;a href="https://semaphoreci.com/docs/servers-and-deploys-api.html#project_servers"&gt;Servers API&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Check branch build status&lt;/p&gt;

&lt;p&gt;First, we need to check if a Semaphore build completed succesfully on a branch. In other words, let's see if the PR is "green" (build passed), "yellow" (build pending) or "red" (build failed):&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SemaphoreClient&lt;/span&gt;

  &lt;span class="o"&gt;...&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pr_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr_number&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"/pull-request-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/status"&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;build_number: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"build_number"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;result: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"result"&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;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;As you can see, calling &lt;code&gt;SemaphoreClient.pr_status("1234")&lt;/code&gt; should return a simple hash with &lt;code&gt;build_number&lt;/code&gt; and &lt;code&gt;result&lt;/code&gt;: we'll need these values for later.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Trigger deployment&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;result&lt;/code&gt; is &lt;code&gt;"passed"&lt;/code&gt;, this means that the Semaphore build has passed and can be safely deployed. Of course, it's also possible to deploy a running build (if you're brave enough). Anyway, let's add another method which'll use the &lt;code&gt;build_number&lt;/code&gt; that we received in previous call.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SemaphoreClient&lt;/span&gt;

  &lt;span class="o"&gt;...&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trigger_deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;build_number&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"/pull-request-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/builds/"&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;build_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/deploy/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;SERVER_ID&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="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;number: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;result: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"result"&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;span class="k"&gt;end&lt;/span&gt;   
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Ta-da! You've just triggered a deployment to a non-default server without having to click through the Semaphore user interface. Time to pop the champagne?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Check deployment status&lt;/p&gt;

&lt;p&gt;Finally, it'd be good to monitor whether a deployment completed successfully. Let's add one more method:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SemaphoreClient&lt;/span&gt;

  &lt;span class="o"&gt;...&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deploy_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deploy_number&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"/servers/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;SERVER_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/deploys/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;deploy_number&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="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;number: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;result: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"result"&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;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And that's it. It's a very simplistic solution, yet should be enough to serve as a proof of concept for futher integration with Github Actions, a Github/Gitlab/Bitbucket bot or any other tool automating your Semaphore CI workflows.&lt;/p&gt;

&lt;p&gt;Finally, for the record, here's the entire &lt;code&gt;SemaphoreClient&lt;/code&gt; class. Again, please treat it as a basic PoC: it definitely calls for some error handling and, depending on your needs, you might want to fetch more data from the API responses. Anyway, happy integrating with Semaphore!&lt;br&gt;
&lt;/p&gt;

&lt;div class="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;SemaphoreClient&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;

  &lt;span class="no"&gt;PROJECT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SEMAPHORE_PROJECT_ID"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="no"&gt;SERVER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SEMAPHORE_SERVER_ID"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;base_uri&lt;/span&gt; &lt;span class="s2"&gt;"https://semaphoreci.com/api/v1/projects/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="n"&gt;default_params&lt;/span&gt; &lt;span class="ss"&gt;auth_token: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SEMAPHORE_API_AUTH_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pr_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr_number&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"/pull-request-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/status"&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;build_number: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"build_number"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;result: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"result"&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;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trigger_deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;build_number&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"/pull-request-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/builds/"&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;build_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/deploy/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;SERVER_ID&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="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;number: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;result: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"result"&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;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deploy_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deploy_number&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"/servers/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;SERVER_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/deploys/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;deploy_number&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="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;number: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;result: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"result"&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;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;



</description>
      <category>ci</category>
      <category>cd</category>
      <category>semaphoreci</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Fixing Rails' Action Cable logger</title>
      <dc:creator>Mariusz Hausenplas</dc:creator>
      <pubDate>Tue, 21 Apr 2020 18:19:19 +0000</pubDate>
      <link>https://forem.com/xlts/fixing-rails-action-cable-logger-la8</link>
      <guid>https://forem.com/xlts/fixing-rails-action-cable-logger-la8</guid>
      <description>&lt;p&gt;&lt;em&gt;cover photo credit: Niels Heidenreich, &lt;a href="https://www.flickr.com/photos/schoschie/282706867"&gt;https://www.flickr.com/photos/schoschie/282706867&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;this story was originally published on Medium on Nov 4, 2018, &lt;a href="https://medium.com/@xlts/fixing-rails-action-cable-logger-7fd0597251cc?source=friends_link&amp;amp;sk=1fb9160c6565f036c53316a883596367"&gt;see link&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Action Cable has been with us for a couple of years now, and seems to be doing a pretty good job at being a simple, out-of-the-box Rails-driven Web Sockets solution. While it’s not as mature as Rails’ standard REST interface, it actually rather seamlessly, just as it should.&lt;/p&gt;

&lt;p&gt;However, when developing Action Cable projects you might have noticed its “interesting” approach towards logging. After making a couple of Web Socket requests to your Action Cable backend, you may see a rather loose and chaotic stream of logs, all fired at different phases of request/response handling and lacking a common structure. The “standard” Rails REST middleware has become a subject of criticism and gems such as Lograge openly define themselves as&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“attempting to bring sanity to Rails’ noisy and unusable, unparsable (…) default logging output.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And, well, Action Cable is also affected.&lt;/p&gt;

&lt;p&gt;So let’s have a look. Here are a few logs generated by Action Cable during the opening of Web Sockets connection, subscribing to a channel and calling a Web Socket method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET "/ws" [WebSocket] for 127.0.0.1 at 2018-11-03 18:25:00 +0200
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)

...

ApiChannel is streaming from api:c0f1054d-3e2e-47be-bef0-9d66f3f2ac70

...

[ActionCable] Broadcasting to api:41196d5e-cab3-4fa3-bbef-9b5d400caae7: {:params=&amp;gt;{:my_key=&amp;gt;"my_value", :current_status=&amp;gt;"online"}}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That looks pretty wild. There are request data logged here and there, but in other cases we’re left with vague debug-like messages which aren’t really informative. Honestly speaking, it looks rather random. Or does it? Here are some examples from the source code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# action_cable/connection/subscriptions.rb&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="s2"&gt;"Received unrecognized command in &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# action_cable/channel/base.rb&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="s2"&gt;"Unable to process &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;action_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# action_cable/connection/message_buffer.rb&lt;/span&gt;

&lt;span class="n"&gt;connection&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;error&lt;/span&gt; &lt;span class="s2"&gt;"Couldn't handle non-string message: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;message&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Well, it kind of does. Almost feels as if somebody forgot to remove his/her own development-only debug logs. Unfortunately, multiple Action Cable source code files are littered with those arbitrary &lt;code&gt;logger&lt;/code&gt; calls. So how do we fix it? First, let's think about how we can silence all unnecessary logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silencing Action Cable logs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1 (bad): Monkey-patch everything.
&lt;/h3&gt;

&lt;p&gt;Taking advantage of capabilities of Ruby, we can reopen Action Cable methods and remove/adjust all redundant &lt;code&gt;logger&lt;/code&gt; calls. Of course, this is tedious and may break whenever there is an update to Action Cable's source code but at the same time, it is least intrusive in a sense that we're not touching the default &lt;code&gt;logger&lt;/code&gt; implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Try to do it systematically.
&lt;/h3&gt;

&lt;p&gt;Instead of reopening every single Action Cable class in which there is a &lt;code&gt;logger&lt;/code&gt; call, we can override the Action Cable logger and set it to a "dummy" instance which doesn't produce any output. To achieve this, add this to your &lt;code&gt;config/application.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ActionCable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Logger&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="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Obviously you may want to keep default logs in e.g. development only; in such case it’s enough to set this empty logger in &lt;code&gt;config/environments/production.rb&lt;/code&gt; or other environment configuration files.&lt;/p&gt;

&lt;p&gt;Now, this seems like good enough solution to our problem, but bear in mind that it results with virtually all Action Cable logs being disabled. That includes those already present in e.g. custom Connection and Channel classes defined in your app. To get around it, you may want to change all &lt;code&gt;logger&lt;/code&gt; calls (which delegate to &lt;code&gt;ActionCable.server.logger&lt;/code&gt;) to the standard &lt;code&gt;Rails.logger&lt;/code&gt; invocations. In short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="ss"&gt;:info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Authenticating connection"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;would have to be changed to&lt;br&gt;
&lt;/p&gt;

&lt;div class="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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s2"&gt;"Authenticating connection"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  Producing sensible logs
&lt;/h2&gt;

&lt;p&gt;Now, how do we produce sensible logs — e.g. generated whenever a single action finishes processing, just as we’d expect it in REST calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Monkey-patching (again)
&lt;/h3&gt;

&lt;p&gt;Again, monkey-patching is an option, and it should work fine when you just want to have one or two specific logs, e.g. after every subscription and Web Sockets action being called. Just reopen &lt;code&gt;ActionCable::Connection::Subscriptions#execute_command&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="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;ActionCable&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Connection&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Subscriptions&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"subscribe"&lt;/span&gt;
          &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="n"&gt;data&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="s2"&gt;"Registered subscription: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"unsubscribe"&lt;/span&gt;
          &lt;span class="n"&gt;remove&lt;/span&gt; &lt;span class="n"&gt;data&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="s2"&gt;"Removed subscription: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"message"&lt;/span&gt;
          &lt;span class="n"&gt;perform_action&lt;/span&gt; &lt;span class="n"&gt;data&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="s2"&gt;"Performed action: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&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;error&lt;/span&gt; &lt;span class="s2"&gt;"Received unrecognized command in &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="si"&gt;}&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;rescue&lt;/span&gt; &lt;span class="no"&gt;Exception&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="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;error&lt;/span&gt; &lt;span class="s2"&gt;"Could not execute command from (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&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;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;backtrace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="s2"&gt;" | "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&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="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;(BTW yes, this &lt;code&gt;e.backtrace.first(5).join(" | ")&lt;/code&gt; is in the actual Action Cable source code)&lt;/p&gt;

&lt;p&gt;This feels pretty good since we’re only reopening a single method handling multiple types of calls: we killed two birds with one stone by adding logs to &lt;code&gt;subscribe&lt;/code&gt;, &lt;code&gt;unsubscribe&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt; actions, and we're able to produce an error log. Now we can do a couple more tweaks: introduce common payload, calculate processing times etc., but again: this would work until we update Rails and there happens to be a change introduced in this very method, or the method could just be removed because of some refactoring. So can we do better?&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: ActiveSupport::Notifications to the rescue
&lt;/h3&gt;

&lt;p&gt;Thankfully, Rails has introduced the &lt;a href="https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html"&gt;&lt;code&gt;ActiveSupport::Notifications&lt;/code&gt;&lt;/a&gt; API which can be neatly used to define and subscribe to arbitrary events, defined as blocks of code. There are plenty of guides to &lt;code&gt;ActiveSupport::Notifications&lt;/code&gt;, but what's most important to us is that in Action Cable source code itself there already are some pre-instrumented parts of code which we can subscribe to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ActionCable::Server::Broadcasting&lt;/code&gt; includes &lt;code&gt;broadcast.action_cable&lt;/code&gt; instrumentation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ActionCable::Channel::Base&lt;/code&gt; includes &lt;code&gt;perform_action.action_cable&lt;/code&gt;, &lt;code&gt;transmit.action_cable&lt;/code&gt;, &lt;code&gt;transmit_subscription_confirmation.action_cable&lt;/code&gt;, &lt;code&gt;transmit_subscription_rejection.action_cable&lt;/code&gt; instrumentations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means we can safely “intercept” any of those messages and produce a log whenever it has finished processing. How do we do that? As an addition to “regular” event subscriptions, Rails also provides a special &lt;a href="https://api.rubyonrails.org/classes/ActiveSupport/LogSubscriber.html"&gt;&lt;code&gt;ActiveSupport::LogSubscriber&lt;/code&gt;&lt;/a&gt; interface designed to consume notifications and generate logs as the only output. For demonstration purposes, let's subscribe to &lt;code&gt;perform_action.action_cable&lt;/code&gt;. Let's define a separate initializer, e.g. &lt;code&gt;my_action_cable_log_subscriber.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="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;MyActionCableLogSubscriber&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;LogSubscriber&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="s2"&gt;"[Action Cable] &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:channel_class&lt;/span&gt;&lt;span class="p"&gt;]&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (Duration: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duration&lt;/span&gt;&lt;span class="si"&gt;}&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="no"&gt;MyActionCableLogSubscriber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach_to&lt;/span&gt; &lt;span class="ss"&gt;:action_cable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;As you can see, we had to create a class inheriting from &lt;code&gt;ActiveSupport::LogSubscriber&lt;/code&gt;. In it we'd have to define a method named exactly as the instrumentation name, in this example: &lt;code&gt;perform_action&lt;/code&gt;. This method accepts a single event argument which is an &lt;code&gt;ActiveSupport::Notifications::event&lt;/code&gt; object containing data defined in instrumentation itself and some additional calculated metrics (e.g. &lt;code&gt;duration&lt;/code&gt;). Finally, we can generate an &lt;code&gt;info&lt;/code&gt; log - the method delegates to the &lt;code&gt;ActiveSupport&lt;/code&gt; logger (&lt;code&gt;Rails.logge&lt;/code&gt;r by default). Feel free to experiment with it, e.g. by creating an &lt;code&gt;error&lt;/code&gt; log in case of an exception:&lt;br&gt;
&lt;/p&gt;

&lt;div class="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;MyActionCableLogSubscriber&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;LogSubscriber&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;base_log&lt;/span&gt; &lt;span class="o"&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:channel_class&lt;/span&gt;&lt;span class="p"&gt;]&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:exception_object&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;exception&lt;/span&gt;
      &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="s2"&gt;"[Action Cable - Failure] &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;base_log&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (Error: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;exception&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="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="s2"&gt;"[Action Cable] &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;base_log&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (Duration: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duration&lt;/span&gt;&lt;span class="si"&gt;}&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="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;MyActionCableLogSubscriber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach_to&lt;/span&gt; &lt;span class="ss"&gt;:action_cable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This will produce following logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Action Cable] MyChannel - my_method (Duration: 5.57)
...
[Action Cable - Failure] MyChannel - my_method (Error: Invalid)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Similarly, we could add other methods such as &lt;code&gt;broadcast&lt;/code&gt; or &lt;code&gt;transmit&lt;/code&gt;, but keep in mind that they need to match the instrumentation name. Then, all we need to do is to register our subscriber to &lt;code&gt;action_cable&lt;/code&gt; scope by calling &lt;code&gt;attach_to&lt;/code&gt;. Pretty simple, eh?&lt;/p&gt;

&lt;p&gt;Unfortunately, not everything is instrumented: we’re still lacking instrumentations in &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;close&lt;/code&gt;, &lt;code&gt;subscribed&lt;/code&gt; and &lt;code&gt;unsubscribed&lt;/code&gt; actions. Fixing this requires (again) monkey patching or fixing this once and for all in Action Cable source code.&lt;/p&gt;

&lt;p&gt;Happy coding with Action Cable!&lt;/p&gt;

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