Ruby and NewRelic APM

Published: Jul 25, 2023 by Isaac Johnson

I have been asked about instrumenting Ruby code for New Relic a lot lately and figured now might be a good time to cover some of the scalable patterns.

Today we’ll set up a sample Ruby app configured to access a sample PostgreSQL database. We will then setup APM instrumentation to New Relic; first natively, then through zipkin and lastly using OpenTelemetry and Kubernetes.

We have a lot to cover, so let’s get started!

Setting up a Ruby app

/content/images/2023/07/rubydemo-24.png

First, we’ll create a Github repo with a Readme and MIT license

/content/images/2023/07/rubydemo-01.png

We’ll clone it down locally

builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://github.com/idjohnson/demo-containerized-app.git
Cloning into 'demo-containerized-app'...
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (5/5), done.
Unpacking objects: 100% (5/5), 1.97 KiB | 1.97 MiB/s, done.
remote: Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
builder@DESKTOP-QADGF36:~/Workspaces$ cd demo-containerized-app/
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ ls
LICENSE  README.md

Now let’s create a Dockerfile that would run an app. The next few steps started from Don Schencks Demo App should you want to look at the source.

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ vi Dockerfile
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ cat Dockerfile
FROM ruby:latest

# throw errors if Gemfile has been modified
RUN bundle config --global frozen 1

WORKDIR /usr/src/app/

COPY Gemfile Gemfile.lock ./
RUN bundle install

ADD . /usr/src/app/

EXPOSE 4000

CMD ["ruby", "/usr/src/app/helloworld.rb"]

We need a Gemfile and Gemfile.lock

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ cat Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"
gem "sinatra"

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ cat Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    mustermann (1.0.3)
    rack (2.0.6)
    rack-protection (2.0.5)
      rack
    sinatra (2.0.5)
      mustermann (~> 1.0)
      rack (~> 2.0)
      rack-protection (= 2.0.5)
      tilt (~> 2.0)
    tilt (2.0.9)

PLATFORMS
  ruby

DEPENDENCIES
  sinatra

BUNDLED WITH
   1.17.1

Lastly, and most importantly, a hello world app

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ cat helloworld.rb
require 'sinatra'

set :port, 4000
set :bind, '0.0.0.0'

get '/' do
    'Hello World!'
end

I’ll now build it locally. You do not need to specify a tag but I do it out of habit.

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ docker build -t hello-world-ruby:0.0.1 .
[+] Building 19.7s (10/11)
 => [internal] load build definition from Dockerfile                                                               0.0s
 => => transferring dockerfile: 293B                                                                               0.0s
 => [internal] load .dockerignore                                                                                  0.0s
 => => transferring context: 2B                                                                                    0.0s
 => [internal] load metadata for docker.io/library/ruby:latest                                                     2.3s
 => [auth] library/ruby:pull token for registry-1.docker.io                                                        0.0s
 => [1/6] FROM docker.io/library/ruby:latest@sha256:05ca729e257fc550f4228714a5f6c9f377d4dab92c30b2213340ddb7eec0  11.1s
 => => resolve docker.io/library/ruby:latest@sha256:05ca729e257fc550f4228714a5f6c9f377d4dab92c30b2213340ddb7eec0b  0.0s
 => => sha256:669a9f71cf3b339ad85ba8d4a5efe7511cd57f59f5c2fd492970e683608a8d8b 1.79kB / 1.79kB                     0.0s
 => => sha256:0c4b46102a5273d695c1a776990d77dba5939119a29806a958495e9645a73f19 8.15kB / 8.15kB                     0.0s
 => => sha256:05ca729e257fc550f4228714a5f6c9f377d4dab92c30b2213340ddb7eec0b597 1.86kB / 1.86kB                     0.0s
 => => sha256:d52e4f012db158bb7c0fe215b98af1facaddcbaee530efd69b1bae07d597b711 49.55MB / 49.55MB                   1.7s
 => => sha256:7dd206bea61ff3e3b54be1c20b58d8475ddd6f89df176146ddb7a2fd2c747ea2 24.03MB / 24.03MB                   3.1s
 => => sha256:2320f9be4a9c605d1ac847cf67cec42b91484a7cf7c94996417a0c7c316deadc 64.11MB / 64.11MB                   2.5s
 => => sha256:6e5565e0ba8dfce32b9049f21ceeb212946e0bb810d94cbd2db94ca61082f657 211.00MB / 211.00MB                 6.0s
 => => extracting sha256:d52e4f012db158bb7c0fe215b98af1facaddcbaee530efd69b1bae07d597b711                          1.1s
 => => sha256:3487b74cbe46c33dbbf395e0b72508708eaf83c574adc8e075d7f445fb076b3e 199B / 199B                         2.6s
 => => sha256:d674e4eae0fcd4fe9b8e66aad48dee351b9b97981f2cf6a9a5742e0ced525cab 34.74MB / 34.74MB                   4.2s
 => => sha256:22b888c5c84f49fcd6d95f0781bf6b03c7931053a76c64e1d517758a4aaa9f05 176B / 176B                         3.5s
 => => extracting sha256:7dd206bea61ff3e3b54be1c20b58d8475ddd6f89df176146ddb7a2fd2c747ea2                          0.5s
 => => extracting sha256:2320f9be4a9c605d1ac847cf67cec42b91484a7cf7c94996417a0c7c316deadc                          1.5s
 => => extracting sha256:6e5565e0ba8dfce32b9049f21ceeb212946e0bb810d94cbd2db94ca61082f657                          4.1s
 => => extracting sha256:3487b74cbe46c33dbbf395e0b72508708eaf83c574adc8e075d7f445fb076b3e                          0.0s
 => => extracting sha256:d674e4eae0fcd4fe9b8e66aad48dee351b9b97981f2cf6a9a5742e0ced525cab                          0.5s
 => => extracting sha256:22b888c5c84f49fcd6d95f0781bf6b03c7931053a76c64e1d517758a4aaa9f05                          0.0s
 => [internal] load build context                                                                                  0.0s
 => => transferring context: 28.69kB                                                                               0.0s
 => [2/6] RUN bundle config --global frozen 1                                                                      1.8s
 => [3/6] WORKDIR /usr/src/app/                                                                                    0.0s
 => [4/6] COPY Gemfile Gemfile.lock ./                                                                             0.0s
 => ERROR [5/6] RUN bundle install                                                                                 3.7s
------
 > [5/6] RUN bundle install:
#10 0.688 Bundler 2.4.10 is running, but your lockfile was generated with 1.17.1. Installing Bundler 1.17.1 and restarting using that version.
#10 3.174 Fetching gem metadata from https://rubygems.org/.
#10 3.224 Fetching bundler 1.17.1
#10 3.478 Installing bundler 1.17.1
#10 3.682 /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/shared_helpers.rb:272:in `search_up': undefined method `untaint' for "/usr/src/app":String (NoMethodError)
#10 3.682
#10 3.682       current  = File.expand_path(SharedHelpers.pwd).untaint
#10 3.682                                                     ^^^^^^^^
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/shared_helpers.rb:259:in `find_file'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/shared_helpers.rb:251:in `find_gemfile'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/shared_helpers.rb:27:in `root'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler.rb:234:in `root'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler.rb:244:in `app_config_path'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler.rb:273:in `settings'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/feature_flag.rb:21:in `block in settings_method'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/cli.rb:97:in `<class:CLI>'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/cli.rb:7:in `<module:Bundler>'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/cli.rb:6:in `<top (required)>'
#10 3.682       from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
#10 3.682       from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/exe/bundle:23:in `block in <top (required)>'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors'
#10 3.682       from /usr/local/bundle/gems/bundler-1.17.1/exe/bundle:22:in `<top (required)>'
#10 3.682       from /usr/local/bin/bundle:25:in `load'
#10 3.682       from /usr/local/bin/bundle:25:in `<main>'
------
executor failed running [/bin/sh -c bundle install]: exit code: 1

The first time through my bundler was out of date (as I use Ruby for this blog)

I needed to update my bundler (which was clear when I tried to do a bundle update)

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ bundle update
Traceback (most recent call last):
        2: from /home/builder/gems/bin/bundle:23:in `<main>'
        1: from /usr/lib/ruby/2.7.0/rubygems.rb:294:in `activate_bin_path'
/usr/lib/ruby/2.7.0/rubygems.rb:275:in `find_spec_for_exe': Could not find 'bundler' (1.17.1) required by your /home/builder/Workspaces/demo-containerized-app/Gemfile.lock. (Gem::GemNotFoundException)
To update to the latest version installed on your system, run `bundle update --bundler`.
To install the missing version, run `gem install bundler:1.17.1`
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ gem install bundler:1.17.1
Fetching bundler-1.17.1.gem
Successfully installed bundler-1.17.1
Parsing documentation for bundler-1.17.1
Installing ri documentation for bundler-1.17.1
Done installing documentation for bundler after 2 seconds
1 gem installed

Then I could run update

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ bundle update
Fetching gem metadata from https://rubygems.org/....
Resolving dependencies...
Using bundler 1.17.1
Fetching ruby2_keywords 0.0.5
Installing ruby2_keywords 0.0.5
Fetching mustermann 3.0.0 (was 1.0.3)
Installing mustermann 3.0.0 (was 1.0.3)
Fetching rack 2.2.7 (was 2.0.6)
Installing rack 2.2.7 (was 2.0.6)
Fetching rack-protection 3.0.6 (was 2.0.5)
Installing rack-protection 3.0.6 (was 2.0.5)
Fetching tilt 2.2.0 (was 2.0.9)
Installing tilt 2.2.0 (was 2.0.9)
Fetching sinatra 3.0.6 (was 2.0.5)
Installing sinatra 3.0.6 (was 2.0.5)
Bundle updated!

My Gemfile.lock now has the proper gems (as of this writing)

$ cat Gemfile.lock
GEM
  remote: https://rubygems.org/
  specs:
    mustermann (3.0.0)
      ruby2_keywords (~> 0.0.1)
    rack (2.2.7)
    rack-protection (3.0.6)
      rack
    ruby2_keywords (0.0.5)
    sinatra (3.0.6)
      mustermann (~> 3.0)
      rack (~> 2.2, >= 2.2.4)
      rack-protection (= 3.0.6)
      tilt (~> 2.0)
    tilt (2.2.0)

PLATFORMS
  ruby

DEPENDENCIES
  sinatra

BUNDLED WITH
   1.17.1

Let’s try again

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ docker build -t hello-world-ruby:0.0.2 .
[+] Building 5.0s (9/10)
 => [internal] load build definition from Dockerfile                                                                                0.0s
 => => transferring dockerfile: 38B                                                                                                 0.0s
 => [internal] load .dockerignore                                                                                                   0.0s
 => => transferring context: 2B                                                                                                     0.0s
 => [internal] load metadata for docker.io/library/ruby:latest                                                                      0.7s
 => [1/6] FROM docker.io/library/ruby:latest@sha256:05ca729e257fc550f4228714a5f6c9f377d4dab92c30b2213340ddb7eec0b597                0.0s
 => [internal] load build context                                                                                                   0.0s
 => => transferring context: 2.77kB                                                                                                 0.0s
 => CACHED [2/6] RUN bundle config --global frozen 1                                                                                0.0s
 => CACHED [3/6] WORKDIR /usr/src/app/                                                                                              0.0s
 => [4/6] COPY Gemfile Gemfile.lock ./                                                                                              0.0s
 => ERROR [5/6] RUN bundle install                                                                                                  3.6s
------
 > [5/6] RUN bundle install:
#9 0.480 Bundler 2.4.10 is running, but your lockfile was generated with 1.17.1. Installing Bundler 1.17.1 and restarting using that version.
#9 3.101 Fetching gem metadata from https://rubygems.org/.
#9 3.151 Fetching bundler 1.17.1
#9 3.401 Installing bundler 1.17.1
#9 3.594 /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/shared_helpers.rb:272:in `search_up': undefined method `untaint' for "/usr/src/app":String (NoMethodError)
#9 3.594
#9 3.594       current  = File.expand_path(SharedHelpers.pwd).untaint
#9 3.594                                                     ^^^^^^^^
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/shared_helpers.rb:259:in `find_file'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/shared_helpers.rb:251:in `find_gemfile'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/shared_helpers.rb:27:in `root'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler.rb:234:in `root'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler.rb:244:in `app_config_path'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler.rb:273:in `settings'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/feature_flag.rb:21:in `block in settings_method'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/cli.rb:97:in `<class:CLI>'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/cli.rb:7:in `<module:Bundler>'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/cli.rb:6:in `<top (required)>'
#9 3.594        from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
#9 3.594        from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/exe/bundle:23:in `block in <top (required)>'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors'
#9 3.594        from /usr/local/bundle/gems/bundler-1.17.1/exe/bundle:22:in `<top (required)>'
#9 3.594        from /usr/local/bin/bundle:25:in `load'
#9 3.594        from /usr/local/bin/bundle:25:in `<main>'
------
executor failed running [/bin/sh -c bundle install]: exit code: 1

I read that it could be due to an old Gemfile lock. I removed and tried again.

this worked:

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ rm Gemfile.lock
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ bundle install
Fetching gem metadata from https://rubygems.org/....
Resolving dependencies...
Using bundler 2.2.26
Using ruby2_keywords 0.0.5
Using rack 2.2.7
Using tilt 2.2.0
Using mustermann 3.0.0
Using rack-protection 3.0.6
Using sinatra 3.0.6
Bundle complete! 1 Gemfile dependency, 7 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ docker build -t hello-world-ruby:0.0.3 .
[+] Building 6.6s (11/11) FINISHED
 => [internal] load build definition from Dockerfile                                                                                0.0s
 => => transferring dockerfile: 38B                                                                                                 0.0s
 => [internal] load .dockerignore                                                                                                   0.0s
 => => transferring context: 2B                                                                                                     0.0s
 => [internal] load metadata for docker.io/library/ruby:latest                                                                      0.7s
 => [1/6] FROM docker.io/library/ruby:latest@sha256:05ca729e257fc550f4228714a5f6c9f377d4dab92c30b2213340ddb7eec0b597                0.0s
 => [internal] load build context                                                                                                   0.0s
 => => transferring context: 2.78kB                                                                                                 0.0s
 => CACHED [2/6] RUN bundle config --global frozen 1                                                                                0.0s
 => CACHED [3/6] WORKDIR /usr/src/app/                                                                                              0.0s
 => [4/6] COPY Gemfile Gemfile.lock ./                                                                                              0.0s
 => [5/6] RUN bundle install                                                                                                        5.0s
 => [6/6] ADD . /usr/src/app/                                                                                                       0.0s
 => exporting to image                                                                                                              0.1s
 => => exporting layers                                                                                                             0.1s
 => => writing image sha256:48e32887d4af01521acee8c0d86bbbcaa03392bc87728d15bd9d01829eabcea5                                        0.0s
 => => naming to docker.io/library/hello-world-ruby:0.0.3                                                                           0.0s

Now let’s pause and create our first commit before we get too far.

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git add -A
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   Dockerfile
        new file:   Gemfile
        new file:   Gemfile.lock
        new file:   helloworld.rb

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git commit -m "First Pass"
[main f899da3] First Pass
 4 files changed, 55 insertions(+)
 create mode 100644 Dockerfile
 create mode 100644 Gemfile
 create mode 100644 Gemfile.lock
 create mode 100644 helloworld.rb
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 16 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 1.06 KiB | 1.06 MiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To https://github.com/idjohnson/demo-containerized-app.git
   2bb51f9..f899da3  main -> main

I can now try and run it

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ docker run -p 4000:4000 hello-world-ruby:0.0.3
/usr/local/bundle/gems/rack-2.2.7/lib/rack/handler.rb:45:in `pick': Couldn't find handler for: thin, falcon, puma, HTTP, webrick. (LoadError)
        from /usr/local/bundle/gems/sinatra-3.0.6/lib/sinatra/base.rb:1526:in `run!'
        from /usr/local/bundle/gems/sinatra-3.0.6/lib/sinatra/main.rb:47:in `block in <module:Sinatra>'

Seems we missed Puma. We’ll add and update the lock with bundler

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ vi Gemfile
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git diff Gemfile
diff --git a/Gemfile b/Gemfile
index b86565d..2edd1ed 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,4 +5,5 @@ source "https://rubygems.org"
 git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

 # gem "rails"
+gem 'puma'
 gem "sinatra"
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ bundle install
Fetching gem metadata from https://rubygems.org/....
Resolving dependencies...
Using bundler 2.2.26
Using ruby2_keywords 0.0.5
Using rack 2.2.7
Using tilt 2.2.0
Using mustermann 3.0.0
Using rack-protection 3.0.6
Fetching nio4r 2.5.9
Using sinatra 3.0.6
Installing nio4r 2.5.9 with native extensions
Fetching puma 6.3.0
Installing puma 6.3.0 with native extensions
Bundle complete! 2 Gemfile dependencies, 9 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

Then I’ll build fresh and try again

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ docker build -t hello-world-ruby:0.0.4 .
[+] Building 13.4s (12/12) FINISHED
 => [internal] load build definition from Dockerfile                                                                                0.0s
 => => transferring dockerfile: 38B                                                                                                 0.0s
 => [internal] load .dockerignore                                                                                                   0.0s
 => => transferring context: 2B                                                                                                     0.0s
 => [internal] load metadata for docker.io/library/ruby:latest                                                                      1.4s
 => [auth] library/ruby:pull token for registry-1.docker.io                                                                         0.0s
 => [internal] load build context                                                                                                   0.0s
 => => transferring context: 6.62kB                                                                                                 0.0s
 => [1/6] FROM docker.io/library/ruby:latest@sha256:05ca729e257fc550f4228714a5f6c9f377d4dab92c30b2213340ddb7eec0b597                0.0s
 => CACHED [2/6] RUN bundle config --global frozen 1                                                                                0.0s
 => CACHED [3/6] WORKDIR /usr/src/app/                                                                                              0.0s
 => [4/6] COPY Gemfile Gemfile.lock ./                                                                                              0.0s
 => [5/6] RUN bundle install                                                                                                       11.0s
 => [6/6] ADD . /usr/src/app/                                                                                                       0.0s
 => exporting to image                                                                                                              0.2s
 => => exporting layers                                                                                                             0.2s
 => => writing image sha256:0f24cdc43153ba58816d7a4172c03d6dfa18e2184bd850c4a34acf855cbb161c                                        0.0s
 => => naming to docker.io/library/hello-world-ruby:0.0.4                                                                           0.0s
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ docker run -p 4000:4000 hello-world-ruby:0.0.4
== Sinatra (v3.0.6) has taken the stage on 4000 for development with backup from Puma
Puma starting in single mode...
* Puma version: 6.3.0 (ruby 3.2.2-p53) ("Mugi No Toki Itaru")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 1
* Listening on http://0.0.0.0:4000
Use Ctrl-C to stop

/content/images/2023/07/rubydemo-02.png

Database

/content/images/2023/07/rubydemo-24.png

For the next part, I’ll go to a Linux host where I have PostgreSQL already running. There I will create a user for the demo app

postgres@isaac-MacBookAir:~$ createuser --pwprompt demoapp
Enter password for new role:
Enter it again:

We’ll now create a simple database with a table and a row of data to query.

postgres@isaac-MacBookAir:~$ createdb demoapp
postgres@isaac-MacBookAir:~$ psql
psql (12.15 (Ubuntu 12.15-0ubuntu0.20.04.1))
Type "help" for help.

postgres=# grant all privileges on database demoapp to demoapp;
GRANT

postgres=# \c demoapp
You are now connected to database "demoapp" as user "postgres".
demoapp=# create table demotable (id INTEGER PRIMARY KEY, name VARCHAR);
CREATE TABLE

demoapp=# INSERT INTO demotable VALUES (1, 'Hello World');
INSERT 0 1
demoapp=# SELECT * FROM demotable;
 id |    name
----+-------------
  1 | Hello World
(1 row)

Before I can add PSQL to the Docker file, the Gem will need libpq-dev locally for some libraries

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ sudo apt update && sudo apt install libpq-dev
[sudo] password for builder:
Get:1 https://apt.releases.hashicorp.com focal InRelease [12.9 kB]
Get:2 https://packages.microsoft.com/repos/azure-cli focal InRelease [3575 B]
Err:1 https://apt.releases.hashicorp.com focal InRelease
... snip ...

I added the ‘pg’ gem and did a bundle install

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git diff
diff --git a/Gemfile b/Gemfile
index b86565d..fe681c4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,4 +5,6 @@ source "https://rubygems.org"
 git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

 # gem "rails"
+gem 'pg'
+gem 'puma'
 gem "sinatra"
diff --git a/Gemfile.lock b/Gemfile.lock
index 272decb..8cffe25 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -3,6 +3,10 @@ GEM
   specs:
     mustermann (3.0.0)
       ruby2_keywords (~> 0.0.1)
+    nio4r (2.5.9)
+    pg (1.5.3)
+    puma (6.3.0)
+      nio4r (~> 2.0)
     rack (2.2.7)
     rack-protection (3.0.6)
       rack
@@ -18,6 +22,8 @@ PLATFORMS
   x86_64-linux

 DEPENDENCIES
+  pg
+  puma
   sinatra

 BUNDLED WITH
diff --git a/helloworld.rb b/helloworld.rb
index 050c9f7..a4c62c4 100644
--- a/helloworld.rb
+++ b/helloworld.rb
@@ -1,8 +1,31 @@

The ruby code then used the pg gem to connect to the local database and fetch rows

$ cat helloworld.rb
require 'sinatra'
require 'pg'

set :port, 4000
set :bind, '0.0.0.0'

get '/' do
    begin
        # Initialize connection variables.
        host = String('192.168.1.78')
        database = String('demoapp')
        user = String('demoapp')
        password = String('demopass')

        # Initialize connection object.
        connection = PG::Connection.new(:host => host, :user => user, :dbname => database, :port => '5432', :password => password)
        puts 'Successfully created connection to database.'

        resultSet = connection.exec('SELECT * from demotable;')
        resultSet.each do |row|
            puts 'Data row = (%s, %s)' % [row['id'], row['name']]
        end

    rescue PG::Error => e
        puts e.message

    ensure
        connection.close if connection
    end
    'Hello World!'
end

Doing a test showed we might have missed a permission or needed to flush permissions in postgres

/content/images/2023/07/rubydemo-03.png

postgres@isaac-MacBookAir:~$ psql -d demoapp
psql (12.15 (Ubuntu 12.15-0ubuntu0.20.04.1))
Type "help" for help.

demoapp=# GRANT SELECT ON ALL TABLES IN SCHEMA public TO demoapp;
GRANT
demoapp=# GRANT ALL ON ALL TABLES IN SCHEMA public TO demoapp;
GRANT

That seemed to work. We’ll mark that part 2 and push it up

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git add Gemfile
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git add Gemfile.lock
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git add helloworld.rb
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git commit -m "part 2"
[main cb6610d] part 2
 3 files changed, 32 insertions(+), 1 deletion(-)
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 16 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 886 bytes | 886.00 KiB/s, done.
Total 5 (delta 3), reused 0 (delta 0)
remote: Resolving deltas: 100% (3/3), completed with 3 local objects.
To https://github.com/idjohnson/demo-containerized-app.git
   f899da3..cb6610d  main -> main

I’ll make one more tweak to just show the row to the user:

require 'sinatra'
require 'pg'
    
set :port, 4000
set :bind, '0.0.0.0'

get '/' do
    begin
        # Initialize connection variables.
        host = String('192.168.1.78')
        database = String('demoapp')
        user = String('demoapp')
        password = String('demopass')
    
        # Initialize connection object.
        connection = PG::Connection.new(:host => host, :user => user, :dbname => database, :port => '5432', :password => password)
        puts 'Successfully created connection to database.'
    
        resultSet = connection.exec('SELECT * from demotable;')
        outStr = "<table><tr><th>id</th><th>name</th></tr>"
        resultSet.each do |row|
            puts 'Data row = (%s, %s)' % [row['id'], row['name']]
            row_data = "<tr><td>#{row['id']}</td><td>#{row['name']}}</td></tr>"
            outStr += "#{row_data}"
        end
        outStr += "</table>"
        outStr
    
    rescue PG::Error => e
        puts e.message
    
    ensure
        connection.close if connection
    end
end

and test

/content/images/2023/07/rubydemo-05.png

I’ll push as part 3

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git add -A
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git commit -m "part 3"
[main 92c25c7] part 3
 1 file changed, 5 insertions(+), 1 deletion(-)
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 406 bytes | 406.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/idjohnson/demo-containerized-app.git
   cb6610d..92c25c7  main -> main

New Relic APM

/content/images/2023/07/rubydemo-25.png

First, we need the NR License Key (or API Key). We can get that from “API Keys” in Administration

/content/images/2023/07/rubydemo-06.png

I’ll add the NR Gems

diff --git a/Gemfile b/Gemfile
index fe681c4..4e2c2c1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,3 +8,6 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
 gem 'pg'
 gem 'puma'
 gem "sinatra"
+
+gem 'newrelic_rpm'
+gem 'newrelic-infinite_tracing'
\ No newline at end of file
diff --git a/Gemfile.lock b/Gemfile.lock

and then bundle install

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ bundle install
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
Using bundler 2.2.26
Using ruby2_keywords 0.0.5
Using nio4r 2.5.9
Using pg 1.5.3
Fetching newrelic_rpm 9.3.1
Using tilt 2.2.0
Using rack 2.2.7
Using rack-protection 3.0.6
Fetching google-protobuf 3.23.4 (x86_64-linux)
Using puma 6.3.0
Using mustermann 3.0.0
Using sinatra 3.0.6
Installing newrelic_rpm 9.3.1
Installing google-protobuf 3.23.4 (x86_64-linux)
Fetching googleapis-common-protos-types 1.7.0
Installing googleapis-common-protos-types 1.7.0
Fetching grpc 1.56.2 (x86_64-linux)
Installing grpc 1.56.2 (x86_64-linux)
Fetching newrelic-infinite_tracing 9.3.1
Installing newrelic-infinite_tracing 9.3.1
Bundle complete! 5 Gemfile dependencies, 15 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

I can now use the local newrelic binary to create the ‘newrelic.yml’ file

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ newrelic install --license_key="***********mykey***********NRAL" "FBDemoApp"

Installed a default configuration file at
/home/builder/Workspaces/demo-containerized-app/newrelic.yml.

Visit support.newrelic.com if you are experiencing installation issues.

Or you can use the boilerplate yaml.

Our Dockerfile allready adds “.” so that should bring in the YAML with our code.

Let’s build and run the Dockerfile

/content/images/2023/07/rubydemo-07.png

And I can now see an entry in New Relic APM

/content/images/2023/07/rubydemo-08.png

Make sure to set the transaction type to “All” to see example runs

/content/images/2023/07/rubydemo-09.png

Before we go and commit things, realize that the New Relic license key is plain text in the newrelic.yml. That is probably something we do not wish to check in.

We’ll add that to our gitignore

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ echo "newrelic.yml" >> .gitignore
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   .gitignore
        modified:   Gemfile
        modified:   Gemfile.lock
        modified:   helloworld.rb

no changes added to commit (use "git add" and/or "git commit -a")

We’ll make this Verison 4

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   .gitignore
        modified:   Gemfile
        modified:   Gemfile.lock
        modified:   helloworld.rb

no changes added to commit (use "git add" and/or "git commit -a")
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git add -A
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git commit -m "With NewRelic"
[main fa12c02] With NewRelic
 4 files changed, 18 insertions(+)
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ git push
Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 16 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 870 bytes | 870.00 KiB/s, done.
Total 6 (delta 4), reused 0 (delta 0)
remote: Resolving deltas: 100% (4/4), completed with 4 local objects.
To https://github.com/idjohnson/demo-containerized-app.git
   92c25c7..fa12c02  main -> main

We do not need to bundle the YAML. In fact, that is likely a bad idea. For the License Key and Name, we can just as well use the environment variables.

I’ll build a new container, sans newrelic.yaml

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ rm newrelic.yml
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ docker build -t hello-world-ruby:0.1.1 .
[+] Building 2.2s (12/12) FINISHED
 => [internal] load build definition from Dockerfile                                                                                0.0s
 => => transferring dockerfile: 38B                                                                                                 0.0s
 => [internal] load .dockerignore                                                                                                   0.0s
 => => transferring context: 2B                                                                                                     0.0s
 => [internal] load metadata for docker.io/library/ruby:latest                                                                      1.5s
 => [auth] library/ruby:pull token for registry-1.docker.io                                                                         0.0s
 => [1/6] FROM docker.io/library/ruby:latest@sha256:05ca729e257fc550f4228714a5f6c9f377d4dab92c30b2213340ddb7eec0b597                0.0s
 => [internal] load build context                                                                                                   0.0s
 => => transferring context: 11.28kB                                                                                                0.0s
 => CACHED [2/6] RUN bundle config --global frozen 1                                                                                0.0s
 => CACHED [3/6] WORKDIR /usr/src/app/                                                                                              0.0s
 => CACHED [4/6] COPY Gemfile Gemfile.lock ./                                                                                       0.0s
 => CACHED [5/6] RUN bundle install                                                                                                 0.0s
 => [6/6] ADD . /usr/src/app/                                                                                                       0.0s
 => exporting to image                                                                                                              0.1s
 => => exporting layers                                                                                                             0.0s
 => => writing image sha256:169bdf90d081e63bc5733eaa759b597c11822dc3399df60dcc2bdf72e5c704fa                                        0.0s
 => => naming to docker.io/library/hello-world-ruby:0.1.1                                                                           0.0s

I can now run it while passing in the vars

$ docker run -e NEW_RELIC_LICENSE_KEY=************************NRAL -e NEW_RELIC_APP_NAME='FBDemoApp (Development)' -p 4000:4000 hello-world-ruby:0.1.0

/content/images/2023/07/rubydemo-10.png

And hitting refresh a few times, I can now see that in APM

/content/images/2023/07/rubydemo-11.png

Some wrong paths.

I tried to follow guides to get Zipkin data to NewRelic’s endpoint while adding headers. The AI, guides and searches got me close


require 'sinatra'
require 'pg'
    
set :port, 4000
set :bind, '0.0.0.0'

get '/' do
    begin
        # Initialize connection variables.
        host = String('192.168.1.78')
        database = String('demoapp')
        user = String('demoapp')
        password = String('demopass')
    
        # Initialize connection object.
        connection = PG::Connection.new(:host => host, :user => user, :dbname => database, :port => '5432', :password => password)
        puts 'Successfully created connection to database.'
    
        resultSet = connection.exec('SELECT * from demotable;')
        outStr = "<table><tr><th>id</th><th>name</th></tr>"
        resultSet.each do |row|
            puts 'Data row = (%s, %s)' % [row['id'], row['name']]
            row_data = "<tr><td>#{row['id']}</td><td>#{row['name']}}</td></tr>"
            outStr += "#{row_data}"
        end
        outStr += "</table>"
        outStr
    
    rescue PG::Error => e
        puts e.message
    
    ensure
        connection.close if connection
    end
end

require 'rack'
require 'zipkin-tracer'

ZIPKIN_TRACER_CONFIG = {
  service_name: 'FBSampleApp (Zipkin1)',
  sample_rate: 1.0,
  json_api_host: 'https://trace-api.newrelic.com/trace/v1'
}.freeze

ZipkinTracer::Config.headers = {
  'Content-Type' => 'application/json',
  'Api-Key' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxNRAL',
  'Data-Format' => 'zipkin',
  'Data-Format-Version' => '2'
}

ZipkinTracer::Config.setup do |config|
    config.service_name = 'FBSampleApp (Zipkin)'
    config.collector_host = 'https://trace-api.newrelic.com/trace/v1'
end

use ZipkinTracer::RackHandler, ZIPKIN_TRACER_CONFIG

However, that style of Headers just doesnt exist in zipkin tracer for me

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ docker run -p 4000:4000 hello-world-ruby:0.1.2
/usr/src/app/helloworldzip.rb:47:in `<main>': undefined method `headers=' for ZipkinTracer::Config:Class (NoMethodError)

ZipkinTracer::Config.headers = {
                    ^^^^^^^^^^

This is where sending Zipkin trace data, whether it is via Faraday or Rack or other to just a local ‘zipkin’ endpoint is going to pay dividends.

We’ll be able to test locally with a zipkin instance (just running java and hitting localhost), then using an OTel client configured for NewRelic.

Using OTel Zipkin Exporters And the NewRelic Collector which they pulled from the common collectors - It used to be here

Seems it was dropped just over a year ago

Zipkin

My next goal is to fire up a local Zipkin instance so we can see Zipkin traces

/content/images/2023/07/rubydemo-26.png

$ cat helloworldzip.rb

require 'sinatra'
require 'pg'
    
set :port, 4000
set :bind, '0.0.0.0'

get '/' do
    begin
        # Initialize connection variables.
        host = String('192.168.1.78')
        database = String('demoapp')
        user = String('demoapp')
        password = String('demopass')
    
        # Initialize connection object.
        connection = PG::Connection.new(:host => host, :user => user, :dbname => database, :port => '5432', :password => password)
        puts 'Successfully created connection to database.'
    
        resultSet = connection.exec('SELECT * from demotable;')
        outStr = "<table><tr><th>id</th><th>name</th></tr>"
        resultSet.each do |row|
            puts 'Data row = (%s, %s)' % [row['id'], row['name']]
            row_data = "<tr><td>#{row['id']}</td><td>#{row['name']}}</td></tr>"
            outStr += "#{row_data}"
        end
        outStr += "</table>"
        outStr
    
    rescue PG::Error => e
        puts e.message
    
    ensure
        connection.close if connection
    end
end

require 'rack'
require 'zipkin-tracer'

ZIPKIN_TRACER_CONFIG = {
  service_name: 'FBSampleApp',
  sample_rate: 1.0,
  json_api_host: 'http://localhost:9411'
}.freeze

use ZipkinTracer::RackHandler, ZIPKIN_TRACER_CONFIG

I’ll want a local Zipkin which I can run with Docker compose

builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://github.com/openzipkin/zipkin.git
Cloning into 'zipkin'...
remote: Enumerating objects: 211805, done.
remote: Counting objects: 100% (1126/1126), done.
remote: Compressing objects: 100% (245/245), done.
remote: Total 211805 (delta 856), reused 1101 (delta 844), pack-reused 210679
Receiving objects: 100% (211805/211805), 66.91 MiB | 25.24 MiB/s, done.
Resolving deltas: 100% (140600/140600), done.
builder@DESKTOP-QADGF36:~/Workspaces$ cd zipkin/docker/examples$
builder@DESKTOP-QADGF36:~/Workspaces/zipkin/docker/examples$ docker-compose -f docker-compose.yml -f docker-compose-ui.yml up
[+] Running 15/15
 ⠿ zipkin-ui Pulled                                                                                                 2.9s
   ⠿ ce92dcb96d54 Pull complete                                                                                     0.7s
   ⠿ 18be8ff31039 Pull complete                                                                                     0.9s
   ⠿ a0db17684027 Pull complete                                                                                     1.0s
   ⠿ 0aa2486fd915 Pull complete                                                                                     1.1s
   ⠿ e04a8c584b93 Pull complete                                                                                     1.2s
   ⠿ 329c46775779 Pull complete                                                                                     1.3s
 ⠿ zipkin Pulled                                                                                                    4.7s
   ⠿ b255216f6add Pull complete                                                                                     1.2s
   ⠿ 5f6acd91b200 Pull complete                                                                                     2.4s
   ⠿ 2e98d2dd5e93 Pull complete                                                                                     2.5s
   ⠿ 0424e82c174b Pull complete                                                                                     2.6s
   ⠿ 72a912b7d968 Pull complete                                                                                     2.7s
   ⠿ cbe57ac687a3 Pull complete                                                                                     2.8s
   ⠿ 7462568844af Pull complete                                                                                     3.1s
[+] Running 3/2
 ⠿ Network examples_default  Created                                                                                0.8s
 ⠿ Container zipkin          Created                                                                                0.2s
 ⠿ Container zipkin-ui       Created                                                                                0.1s
Attaching to zipkin, zipkin-ui
zipkin     |
zipkin     |                   oo
zipkin     |                  oooo
zipkin     |                 oooooo
zipkin     |                oooooooo
zipkin     |               oooooooooo
zipkin     |              oooooooooooo
zipkin     |            ooooooo  ooooooo
zipkin     |           oooooo     ooooooo
zipkin     |          oooooo       ooooooo
zipkin     |         oooooo   o  o   oooooo
zipkin     |        oooooo   oo  oo   oooooo
zipkin     |      ooooooo  oooo  oooo  ooooooo
zipkin     |     oooooo   ooooo  ooooo  ooooooo
zipkin     |    oooooo   oooooo  oooooo  ooooooo
zipkin     |   oooooooo      oo  oo      oooooooo
zipkin     |   ooooooooooooo oo  oo ooooooooooooo
zipkin     |       oooooooooooo  oooooooooooo
zipkin     |           oooooooo  oooooooo
zipkin     |               oooo  oooo
zipkin     |
zipkin     |      ________ ____  _  _____ _   _
zipkin     |     |__  /_ _|  _ \| |/ /_ _| \ | |
zipkin     |       / / | || |_) | ' / | ||  \| |
zipkin     |      / /_ | ||  __/| . \ | || |\  |
zipkin     |     |____|___|_|   |_|\_\___|_| \_|
zipkin     |
zipkin     | :: version 2.24.2 :: commit 3575817 ::
zipkin     |
zipkin-ui  | nginx: [warn] duplicate extension "woff2", content type: "application/font-woff2", previous content type: "font/woff2" in /etc/nginx/nginx.conf:35
zipkin     | 2023-07-21 22:33:50:315 [armeria-boss-http-*:9411] INFO Server - Serving HTTP at /0.0.0.0:9411 - http://127.0.0.1:9411/

I now have Zipkin running

/content/images/2023/07/rubydemo-12.png

I had some challenges getting the docker running with $ docker run -p 4000:4000 hello-world-ruby:0.1.5 hitting the IP of the other. I tried local NATing, localhost, etc.

172.17.0.1 - - [22/Jul/2023:13:19:15 +0000] "GET / HTTP/1.1" 200 88 0.0290
E, [2023-07-22T13:19:15.204488 #1] ERROR -- : Error while connecting to http://localhost:9411: Faraday::ConnectionFailed with message 'Failed to open TCP connection to localhost:9411 (Cannot assign requested address - connect(2) for "localhost" port 9411)'. Please make sure the URL / port are properly specified for the Zipkin server.

But I mostly want to test the code, not docker.

So when I ran directly

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ ruby helloworldzip.rb
/home/builder/gems/gems/sinatra-3.0.6/lib/sinatra/base.rb:931: warning: constant Tilt::Cache is deprecated
W, [2023-07-22T08:20:01.429775 #19106]  WARN -- : Using a boolean in the Sampled header is deprecated. Consider setting sampled_as_boolean to false
== Sinatra (v3.0.6) has taken the stage on 4000 for development with backup from Puma
Puma starting in single mode...
* Puma version: 6.3.0 (ruby 2.7.0-p0) ("Mugi No Toki Itaru")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 19106
* Listening on http://0.0.0.0:4000
Use Ctrl-C to stop
Successfully created connection to database.
Data row = (1, Hello World)
127.0.0.1 - - [22/Jul/2023:08:20:06 -0500] "GET / HTTP/1.1" 200 88 0.0393
Successfully created connection to database.
Data row = (1, Hello World)
127.0.0.1 - - [22/Jul/2023:08:20:07 -0500] "GET / HTTP/1.1" 200 88 0.0190
Successfully created connection to database.
Data row = (1, Hello World)
127.0.0.1 - - [22/Jul/2023:08:20:08 -0500] "GET / HTTP/1.1" 200 88 0.0423
Successfully created connection to database.
Data row = (1, Hello World)
127.0.0.1 - - [22/Jul/2023:08:20:08 -0500] "GET / HTTP/1.1" 200 88 0.0159
Successfully created connection to database.
Data row = (1, Hello World)
127.0.0.1 - - [22/Jul/2023:08:20:10 -0500] "GET / HTTP/1.1" 200 88 0.0254
^C- Gracefully stopping, waiting for requests to finish

I could see traces

/content/images/2023/07/rubydemo-13.png

If we want a multi-service example, we can use this example repo

I can run the frontend.rb and backend.rb and give it a test

/content/images/2023/07/rubydemo-14.png

And, of course, we can see a dependency map

/content/images/2023/07/rubydemo-15.png

And since we have more interesting data, we can view specifics of the Trace

/content/images/2023/07/rubydemo-16.png

Kubernetes

/content/images/2023/07/rubydemo-27.png

There are a few helm charts out there such as ‘ygqygq2’ here and Openzipkin here.

We’ll use the later:

builder@DESKTOP-QADGF36:~/Workspaces/zipkin-ruby-example$ helm repo add openzipkin https://openzipkin.github.io/zipkin
"openzipkin" has been added to your repositories
builder@DESKTOP-QADGF36:~/Workspaces/zipkin-ruby-example$ helm search repo openzipkin
NAME                    CHART VERSION   APP VERSION     DESCRIPTION
openzipkin/zipkin       0.7.0           2.24.1          A Zipkin helm chart for kubernetes

I can now install

builder@DESKTOP-QADGF36:~/Workspaces/zipkin-ruby-example$ helm install zipkin openzipkin/zipkin
NAME: zipkin
LAST DEPLOYED: Sat Jul 22 08:35:44 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=zipkin,app.kubernetes.io/instance=zipkin" -o jsonpath="{.items[0].metadata.name}")
  export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT

We can see the service was created for us

builder@DESKTOP-QADGF36:~/Workspaces/zipkin-ruby-example$ kubectl get svc zipkin
NAME     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
zipkin   ClusterIP   10.43.138.139   <none>        9411/TCP   19s

I’ll now change the URL

ZIPKIN_TRACER_CONFIG = {
  service_name: 'FBSampleApp',
  sample_rate: 1.0,
  json_api_host: 'http://zipkin.default.svc.cluster.local:9411'
}.freeze

Actually, let’s use an ENV Var like “ZIPKIN_URL”


require 'sinatra'
require 'pg'
    
set :port, 4000
set :bind, '0.0.0.0'

get '/' do
    begin
        # Initialize connection variables.
        host = String('192.168.1.78')
        database = String('demoapp')
        user = String('demoapp')
        password = String('demopass')
    
        # Initialize connection object.
        connection = PG::Connection.new(:host => host, :user => user, :dbname => database, :port => '5432', :password => password)
        puts 'Successfully created connection to database.'
    
        resultSet = connection.exec('SELECT * from demotable;')
        outStr = "<table><tr><th>id</th><th>name</th></tr>"
        resultSet.each do |row|
            puts 'Data row = (%s, %s)' % [row['id'], row['name']]
            row_data = "<tr><td>#{row['id']}</td><td>#{row['name']}}</td></tr>"
            outStr += "#{row_data}"
        end
        outStr += "</table>"
        outStr
    
    rescue PG::Error => e
        puts e.message
    
    ensure
        connection.close if connection
    end
end

require 'rack'
require 'zipkin-tracer'

ZIPKIN_TRACER_CONFIG = {
  service_name: 'FBSampleApp',
  sample_rate: 1.0,
  json_api_host: ENV["ZIPKIN_URL"]
}.freeze

use ZipkinTracer::RackHandler, ZIPKIN_TRACER_CONFIG

I’ll want to deploy this so best to build a version and push to my Harbor CR

builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ docker tag hello-world-ruby:1.2.0 harbor.freshbrewed.science/library/hello-world-ruby:1.2.0
builder@DESKTOP-QADGF36:~/Workspaces/demo-containerized-app$ docker push  harbor.freshbrewed.science/library/hello-world-ruby:1.2.0
The push refers to repository [harbor.freshbrewed.science/library/hello-world-ruby]
93c604d63b20: Pushed
8442fa2dfe02: Pushed
3bf0d91550b9: Pushed
54a0b3e9a828: Pushed
fc0015b23b27: Pushed
aa65f5b2fc19: Pushed
1573adcbec5d: Pushed
24deff53c342: Pushed
28218ecd8008: Pushed
2f66f3254105: Pushed
a72216901005: Pushed
61581d479298: Pushed
1.2.0: digest: sha256:47a329784730c94ca2e28155278963c3f6be494bdf315ff6355e8e178d26731f size: 2834

I can see now

/content/images/2023/07/rubydemo-17.png

We can pull the image

docker pull harbor.freshbrewed.science/library/hello-world-ruby@sha256:47a329784730c94ca2e28155278963c3f6be494bdf315ff6355e8e178d26731f

We can actually just create a deployment on the fly and expose port 4000 on a service

$ kubectl create deployment rubysample --image harbor.freshbrewed.science/library/hello-world-ruby:1.2
.0
deployment.apps/rubysample created
$ kubectl expose deployment rubysample --type ClusterIP --port 4000
service/rubysample exposed

I realized I should have added --env=ZIPKIN_URL=http://zipkin.default.svc.cluster.local:9411 in the kubectl deployment create command.

I can use kubectl edit deployment and add the absent block instead

      containers:
      - env:
        - name: ZIPKIN_URL
          value: "http://zipkin.default.svc.cluster.local:9411"

Or we can just run as a pod and skip deployments

$ kubectl run rubysample --image harbor.freshbrewed.science/library/hello-world-ruby:1.2.0 --restart=Always --env=ZIPKIN_URL="http://zipkin.default.svc.cluster.local:9411"
pod/rubysample created

/content/images/2023/07/rubydemo-19.png

I had to port-forward to zipkin on 9412 since I’m still binding to 9411 for my local zipkin in docker. That said, I can port-forward and see my traces in Zipkin in Kubernetes just as a had when using Docker

/content/images/2023/07/rubydemo-20.png

The traces aren’t very interesting, but it is fetching from an on-prem PostgreSQL and sending traces to the Zipkin service inside K8s

/content/images/2023/07/rubydemo-21.png

Open Telemetry (OTel)

Let’s now add Otel to the mix

/content/images/2023/07/rubydemo-28.png

I’ll install with ‘deployment’ mode

builder@DESKTOP-QADGF36:~/Workspaces/zipkin-ruby-example$ helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
"open-telemetry" already exists with the same configuration, skipping
builder@DESKTOP-QADGF36:~/Workspaces/zipkin-ruby-example$ helm install my-opentelemetry-collector open-telemetry/opentelemetry-collector --set mode=deployment
NAME: my-opentelemetry-collector
LAST DEPLOYED: Sat Jul 22 09:04:21 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
[INFO] as of chart version 0.47.0 the default collector configuration has been updated to use pod IP instead of 0.0.0.0 for its endpoints. See https://github.com/open-telemetry/opentelemetry-helm-charts/blob/main/charts/opentelemetry-collector/UPGRADING.md#0460-to-0470 for details.
[WARNING] As of 0.54.0 Collector chart, the default resource limits are removed. See https://github.com/open-telemetry/opentelemetry-helm-charts/blob/main/charts/opentelemetry-collector/UPGRADING.md#0531-to-0540 for details.

OTel covers the popeular tracing formats, including Zipkin. We can see the service is now running

$ kubectl get svc my-opentelemetry-collector
NAME                         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                                                   AGE
my-opentelemetry-collector   ClusterIP   10.43.184.118   <none>        6831/UDP,14250/TCP,14268/TCP,4317/TCP,4318/TCP,9411/TCP   93s

Routing through Otel

What I want to do next is configure the OTel collector to recieve Zipkin (already set) and to the forward it the Zipkin service in the cluster (just act as a relay)

/content/images/2023/07/rubydemo-28.png

I’ll fetch the CM and add a section:

$ kubectl get cm my-opentelemetry-collector -o yaml > moc.cm.yaml
$ vi moc.cm.yaml
$ cat moc.cm.yaml
$ cat moc.cm.yaml
apiVersion: v1
data:
  relay: |
    exporters:
      zipkin:
        endpoint: "http://zipkin.default.svc.cluster.local:9411/api/v2/spans"
      logging: {}
    extensions:
      health_check: {}
      memory_ballast:
        size_in_percentage: 40
    processors:
      batch: {}
      memory_limiter:
        check_interval: 5s
        limit_percentage: 80
        spike_limit_percentage: 25
    receivers:
      jaeger:
        protocols:
          grpc:
            endpoint: ${env:MY_POD_IP}:14250
          thrift_compact:
            endpoint: ${env:MY_POD_IP}:6831
          thrift_http:
            endpoint: ${env:MY_POD_IP}:14268
      otlp:
        protocols:
          grpc:
            endpoint: ${env:MY_POD_IP}:4317
          http:
            endpoint: ${env:MY_POD_IP}:4318
      prometheus:
        config:
          scrape_configs:
          - job_name: opentelemetry-collector
            scrape_interval: 10s
            static_configs:
            - targets:
              - ${env:MY_POD_IP}:8888
      zipkin:
        endpoint: ${env:MY_POD_IP}:9411
    service:
      extensions:
      - health_check
      - memory_ballast
      pipelines:
        logs:
          exporters:
          - logging
          processors:
          - memory_limiter
          - batch
          receivers:
          - otlp
        metrics:
          exporters:
          - logging
          processors:
          - memory_limiter
          - batch
          receivers:
          - otlp
          - prometheus
        traces:
          exporters:
          - logging
          - zipkin
          processors:
          - memory_limiter
          - batch
          receivers:
          - otlp
          - jaeger
          - zipkin
      telemetry:
        metrics:
          address: ${env:MY_POD_IP}:8888
kind: ConfigMap
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","data":{"relay":"exporters:\n  zipkin:\n    endpoint: \"http://zipkin.default.svc.cluster.local:9411/api/v2/spans\"\n    insecure: true\n  logging: {}\nextensions:\n  health_check: {}\n  memory_ballast:\n    size_in_percentage: 40\nprocessors:\n  batch: {}\n  memory_limiter:\n    check_interval: 5s\n    limit_percentage: 80\n    spike_limit_percentage: 25\nreceivers:\n  jaeger:\n    protocols:\n      grpc:\n        endpoint: ${env:MY_POD_IP}:14250\n      thrift_compact:\n        endpoint: ${env:MY_POD_IP}:6831\n      thrift_http:\n        endpoint: ${env:MY_POD_IP}:14268\n  otlp:\n    protocols:\n      grpc:\n        endpoint: ${env:MY_POD_IP}:4317\n      http:\n        endpoint: ${env:MY_POD_IP}:4318\n  prometheus:\n    config:\n      scrape_configs:\n      - job_name: opentelemetry-collector\n        scrape_interval: 10s\n        static_configs:\n        - targets:\n          - ${env:MY_POD_IP}:8888\n  zipkin:\n    endpoint: ${env:MY_POD_IP}:9411\nservice:\n  extensions:\n  - health_check\n  - memory_ballast\n  pipelines:\n    logs:\n      exporters:\n      - logging\n      processors:\n      - memory_limiter\n      - batch\n      receivers:\n      - otlp\n    metrics:\n      exporters:\n      - logging\n      processors:\n      - memory_limiter\n      - batch\n      receivers:\n      - otlp\n      - prometheus\n    traces:\n      exporters:\n      - logging\n      - zipkin\n      processors:\n      - memory_limiter\n      - batch\n      receivers:\n      - otlp\n      - jaeger\n      - zipkin\n  telemetry:\n    metrics:\n      address: ${env:MY_POD_IP}:8888\n"},"kind":"ConfigMap","metadata":{"annotations":{"meta.helm.sh/release-name":"my-opentelemetry-collector","meta.helm.sh/release-namespace":"default"},"creationTimestamp":"2023-07-22T14:04:21Z","labels":{"app.kubernetes.io/instance":"my-opentelemetry-collector","app.kubernetes.io/managed-by":"Helm","app.kubernetes.io/name":"opentelemetry-collector","app.kubernetes.io/version":"0.81.0","helm.sh/chart":"opentelemetry-collector-0.62.2"},"name":"my-opentelemetry-collector","namespace":"default","resourceVersion":"519924","uid":"e1197248-35c4-4d97-a946-9559ffe3ff90"}}
    meta.helm.sh/release-name: my-opentelemetry-collector
    meta.helm.sh/release-namespace: default
  creationTimestamp: "2023-07-22T14:04:21Z"
  labels:
    app.kubernetes.io/instance: my-opentelemetry-collector
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: opentelemetry-collector
    app.kubernetes.io/version: 0.81.0
    helm.sh/chart: opentelemetry-collector-0.62.2
  name: my-opentelemetry-collector
  namespace: default
  resourceVersion: "533100"
  uid: e1197248-35c4-4d97-a946-9559ffe3ff90

Then apply it

$ kubectl apply -f moc.cm.yaml
configmap/my-opentelemetry-collector configured

Then rotate the collector pod to have it take in the fresh CM

$ kubectl delete pod my-opentelemetry-collector-76dbb7d64b-rcsd4
pod "my-opentelemetry-collector-76dbb7d64b-rcsd4" deleted

Next, I’ll just launch a new pod that will forward to the Otel collector instead of Zipkin:

$ kubectl run rubysampleotel --image harbor.freshbrewed.science/library/hello-world-ruby:1.2.0 --restart=Always --env=ZI
PKIN_URL="http://my-opentelemetry-collector.default.svc.cluster.local:9411"
pod/rubysampleotel created

Now I just need to create a forwarder for the ruby sample app and the kubernetes zipkin to see the results:

$ kubectl port-forward pod/rubysampleotel 4000:4000
Forwarding from 127.0.0.1:4000 -> 4000
Forwarding from [::1]:4000 -> 4000

in another window

$ kubectl port-forward svc/zipkin 9412:9411
Forwarding from 127.0.0.1:9412 -> 9411
Forwarding from [::1]:9412 -> 9411
Handling connection for 9412

/content/images/2023/07/rubydemo-22.png

New Relic

The last mile is to send the traces to NewRelic as well

/content/images/2023/07/rubydemo-29.png

NewRelic abandoned their custom exporter in favour of using OTLP and packing the API key into the header.

We can see an example config here

Key is to set

exporters:
  otlp:
    endpoint: https://otlp.nr-data.net:4317
    headers:
      "api-key": $NEW_RELIC_API_KEY

We can pull down the CM, update and push it back up

$ kubectl get cm my-opentelemetry-collector -o yaml > moc.cm.yaml
$ vi moc.cm.yaml
$ cat moc.cm.yaml | head -n20
$ cat moc.cm.yaml
apiVersion: v1
data:
  relay: |
    exporters:
      zipkin:
        endpoint: "http://zipkin.default.svc.cluster.local:9411/api/v2/spans"
      otlp:
        endpoint: "https://otlp.nr-data.net:4317"
        headers:
          "api-key": **************************NRAL
      logging: {}
    extensions:
      health_check: {}
      memory_ballast:
        size_in_percentage: 40
    processors:
      batch: {}
      memory_limiter:
        check_interval: 5s
        limit_percentage: 80
        spike_limit_percentage: 25
    receivers:
      jaeger:
        protocols:
          grpc:
            endpoint: ${env:MY_POD_IP}:14250
          thrift_compact:
            endpoint: ${env:MY_POD_IP}:6831
          thrift_http:
            endpoint: ${env:MY_POD_IP}:14268
      otlp:
        protocols:
          grpc:
            endpoint: ${env:MY_POD_IP}:4317
          http:
            endpoint: ${env:MY_POD_IP}:4318
      prometheus:
        config:
          scrape_configs:
          - job_name: opentelemetry-collector
            scrape_interval: 10s
            static_configs:
            - targets:
              - ${env:MY_POD_IP}:8888
      zipkin:
        endpoint: ${env:MY_POD_IP}:9411
    service:
      extensions:
      - health_check
      - memory_ballast
      pipelines:
        logs:
          exporters:
          - logging
          processors:
          - memory_limiter
          - batch
          receivers:
          - otlp
        metrics:
          exporters:
          - logging
          processors:
          - memory_limiter
          - batch
          receivers:
          - otlp
          - prometheus
        traces:
          exporters:
          - otlp
          - logging
          - zipkin
          processors:
          - memory_limiter
          - batch
          receivers:
          - otlp
          - jaeger
          - zipkin
      telemetry:
        metrics:
          address: ${env:MY_POD_IP}:8888
kind: ConfigMap
$ kubectl apply -f moc.cm.yaml
configmap/my-opentelemetry-collector configured

I’ll rotate the Otel pod to take effect

$ kubectl delete pods -l app.kubernetes.io/instance=my-opentelemetry-collector
pod "my-opentelemetry-collector-76dbb7d64b-s9z8m" deleted

I can now see traces in both Zipkin and NewRelic

/content/images/2023/07/rubydemo-23.png

Summary

Let’s review what we did here. We built out a Ruby app that can access PostgreSQL. The Database portion wasn’t that important, mostly just to slow up the app. We added the New Relic Gems and showed native direct tracing to New Relic APM.

/content/images/2023/07/20230723_085724-EDIT.jpg

Then, we setup tracing with zipkin-tracer and rack. We installed Zipkin locally as a docker container to show how to use tracing locally. We then deployed Zipkin with Helm into a cluster and deployed our ruby sample app with kubectl run.

We installed the OpenTelemetry into Kubernetes and showed it could be a forwarding service to Zipkin. Lastly, we added the New Relic OTLP endpoint and showed traces simultaneously going to Zipkin and NewRelic.

Benefits of this Model

It is easy enough to see how we can expand this to other systems. By setting up our containers to use Zipkin Traces we can:

  1. Test locally with Docker
  2. Create a Container once - it has a parameter for Zipkin endpoint - no more compiling with specific 3rd Party bindings - this avoids Vendor lock-in
  3. Test in Kubernetes without egressing to an APM (and incurring APM Costs)
  4. Change or Expand APM options by sending traces to the APM that makes the most sense
  5. Migrate to APMs - this, too, avoids Vendor lock-in
NewReplic OpenTelemetry APM Ruby Zipkin

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes