diff --git a/lessons/220/deploy/ruby-app/0-config.yaml b/lessons/220/deploy/ruby-app/0-config.yaml new file mode 100644 index 000000000..c3b66c614 --- /dev/null +++ b/lessons/220/deploy/ruby-app/0-config.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: ruby-app + namespace: default +data: + database.yml: | + default: &default + adapter: postgresql + encoding: unicode + timeout: 5000 + host: postgresql.antonputra.pvt + database: mydb + pool: 50 + username: rails + password: devops123 + + development: + !!merge <<: *default + + production: + !!merge <<: *default diff --git a/lessons/220/deploy/ruby-app/1-deployment.yaml b/lessons/220/deploy/ruby-app/1-deployment.yaml new file mode 100644 index 000000000..17652eae5 --- /dev/null +++ b/lessons/220/deploy/ruby-app/1-deployment.yaml @@ -0,0 +1,86 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ruby-app + namespace: default +spec: + replicas: 2 + strategy: + type: Recreate + selector: + matchLabels: + app: ruby-app + template: + metadata: + labels: + app: ruby-app + spec: + terminationGracePeriodSeconds: 0 + initContainers: + - name: migration + image: aputra/ruby-app-220:v5 + command: ['sh', '-c', "/rails/bin/rake db:migrate"] + volumeMounts: + - name: config + mountPath: /rails/db/config.yml + subPath: config.yml + containers: + - name: ruby-app + image: aputra/ruby-app-220:v5 + ports: + - name: http + containerPort: 8080 + - name: WORKERS_NUM + value: "2" # 0 for single-mode + - name: RAILS_MAX_THREADS + value: "2" + resources: + requests: + memory: 512Mi + cpu: 750m + limits: + memory: 512Mi + cpu: 1000m + readinessProbe: + httpGet: + path: /up + port: http + livenessProbe: + httpGet: + path: /up + port: http + volumeMounts: + - name: config + mountPath: /rails/db/config.yml + subPath: config.yml + volumes: + - name: config + configMap: + name: ruby-app + tolerations: + - effect: NoSchedule + operator: Exists + affinity: + podAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app + operator: In + values: + - ruby-app + topologyKey: "kubernetes.io/hostname" + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node + operator: In + values: + - general + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 diff --git a/lessons/220/deploy/ruby-app/2-service.yaml b/lessons/220/deploy/ruby-app/2-service.yaml new file mode 100644 index 000000000..5aa99e6b7 --- /dev/null +++ b/lessons/220/deploy/ruby-app/2-service.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: ruby-app + namespace: default +spec: + ports: + - name: http + port: 8080 + targetPort: http + selector: + app: ruby-app + type: ClusterIP diff --git a/lessons/220/deploy/ruby-app/3-pod-monitor.yaml b/lessons/220/deploy/ruby-app/3-pod-monitor.yaml new file mode 100644 index 000000000..f12ce1ee4 --- /dev/null +++ b/lessons/220/deploy/ruby-app/3-pod-monitor.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: ruby-app + namespace: monitoring + labels: + prometheus: main +spec: + namespaceSelector: + matchNames: + - default + selector: + matchLabels: + app: ruby-app + podMetricsEndpoints: + - port: http diff --git a/lessons/220/deploy/ruby-app/4-client-pod-monitor.yaml b/lessons/220/deploy/ruby-app/4-client-pod-monitor.yaml new file mode 100644 index 000000000..9952e0301 --- /dev/null +++ b/lessons/220/deploy/ruby-app/4-client-pod-monitor.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: rails-client + namespace: monitoring + labels: + prometheus: main +spec: + namespaceSelector: + matchNames: + - default + selector: + matchLabels: + app: rails-client + podMetricsEndpoints: + - port: metrics diff --git a/lessons/220/ruby-app/.dockerignore b/lessons/220/ruby-app/.dockerignore new file mode 100644 index 000000000..7f5a804c3 --- /dev/null +++ b/lessons/220/ruby-app/.dockerignore @@ -0,0 +1,42 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore CI service files. +/.github + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/lessons/220/ruby-app/.gitattributes b/lessons/220/ruby-app/.gitattributes new file mode 100644 index 000000000..8dc432343 --- /dev/null +++ b/lessons/220/ruby-app/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/lessons/220/ruby-app/.gitignore b/lessons/220/ruby-app/.gitignore new file mode 100644 index 000000000..b637b08c7 --- /dev/null +++ b/lessons/220/ruby-app/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# Temporary files generated by your text editor or operating system +# belong in git's global ignore instead: +# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` + +# Ignore bundler config. +/.bundle +/.idea + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +# Ignore master key for decrypting credentials and more. +/config/master.key diff --git a/lessons/220/ruby-app/.ruby-version b/lessons/220/ruby-app/.ruby-version new file mode 100644 index 000000000..f13c6f452 --- /dev/null +++ b/lessons/220/ruby-app/.ruby-version @@ -0,0 +1 @@ +ruby-3.3.5 diff --git a/lessons/220/ruby-app/Dockerfile b/lessons/220/ruby-app/Dockerfile new file mode 100644 index 000000000..2b98b6070 --- /dev/null +++ b/lessons/220/ruby-app/Dockerfile @@ -0,0 +1,59 @@ +# syntax = docker/dockerfile:1 + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t ruby-app . +# docker run -d -p 8080:8080 --name ruby-app ruby-app + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.3.5 +FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 libpq-dev && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment +ENV RAKE_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git + +# Copy application code +COPY . . + +# Final stage for app image +FROM base + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown -R rails:rails db tmp +USER 1000:1000 + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start the server by default, this can be overwritten at runtime +EXPOSE 8080 +CMD ["bin/iodine", "-p 8080", "config.ru"] diff --git a/lessons/220/ruby-app/Gemfile b/lessons/220/ruby-app/Gemfile new file mode 100644 index 000000000..251297ce2 --- /dev/null +++ b/lessons/220/ruby-app/Gemfile @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'connection_pool' +gem 'pg' +gem 'rake' +gem 'standalone_migrations', require: false + +gem 'oj' +gem 'prometheus-client' +gem 'rack' +gem 'rage-iodine' diff --git a/lessons/220/ruby-app/README.md b/lessons/220/ruby-app/README.md new file mode 100644 index 000000000..cfe57e741 --- /dev/null +++ b/lessons/220/ruby-app/README.md @@ -0,0 +1,81 @@ +# README + +Basic Ruby application. 25x faster than Rails on my Mac M1. + +To setup database run the following commands: +```bash +bin/rake db:create +bin/rake db:migrate +``` + +To run the server: +```bash +bin/iodine -p 8080 +``` + +Defaults +```bash +WORKERS_NUM=2 +RAILS_MAX_THREADS=2 + +``` + +### Local benchmark + +#### `rails-app` +```bash +WORKERS_NUM=2 RAILS_MAX_THREADS=2 RAILS_ENV=production rs +wrk -c 10 -t 2 -d 10 --latency http://localhost:8080/api/devices +Running 10s test @ http://localhost:8080/api/devices + 2 threads and 10 connections + Thread Stats Avg Stdev Max +/- Stdev + Latency 2.12ms 846.12us 31.20ms 94.80% + Req/Sec 2.42k 239.12 2.66k 88.12% + Latency Distribution + 50% 1.94ms + 75% 2.17ms + 90% 2.63ms + 99% 5.35ms + 48603 requests in 10.10s, 46.07MB read +Requests/sec: 4811.04 +Transfer/sec: 4.56MB +``` + +#### `ruby-app` +```bash +WORKERS_NUM=2 RAILS_MAX_THREADS=2 bin/iodine -p 8080 config.ru +wrk -c 10 -t 2 -d 10 --latency http://localhost:8080/api/devices +Running 10s test @ http://localhost:8080/api/devices + 2 threads and 10 connections + Thread Stats Avg Stdev Max +/- Stdev + Latency 142.99us 660.05us 21.06ms 99.01% + Req/Sec 48.48k 9.07k 60.82k 63.00% + Latency Distribution + 50% 83.00us + 75% 112.00us + 90% 160.00us + 99% 798.00us + 965345 requests in 10.01s, 613.14MB read +Requests/sec: 96422.17 +Transfer/sec: 61.24MB +``` + +#### `ruby-app` with docker +```bash +docker build -t ruby-app . +docker run --rm -p 8080:8080 --name ruby-app ruby-app +wrk -c 10 -t 2 -d 10 --latency http://localhost:8080/api/devices +Running 10s test @ http://localhost:8080/api/devices + 2 threads and 10 connections + Thread Stats Avg Stdev Max +/- Stdev + Latency 785.27us 3.26ms 62.81ms 97.92% + Req/Sec 12.66k 3.14k 15.44k 84.50% + Latency Distribution + 50% 351.00us + 75% 430.00us + 90% 614.00us + 99% 12.92ms + 251840 requests in 10.00s, 159.96MB read +Requests/sec: 25176.72 +Transfer/sec: 15.99MB +``` diff --git a/lessons/220/ruby-app/Rakefile b/lessons/220/ruby-app/Rakefile new file mode 100644 index 000000000..4c1375c5e --- /dev/null +++ b/lessons/220/ruby-app/Rakefile @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require 'standalone_migrations' +StandaloneMigrations::Tasks.load_tasks diff --git a/lessons/220/ruby-app/bin/docker-entrypoint b/lessons/220/ruby-app/bin/docker-entrypoint new file mode 100755 index 000000000..cbb10b281 --- /dev/null +++ b/lessons/220/ruby-app/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ] && [ -f /usr/lib/*/libjemalloc.so.2 ]; then + export LD_PRELOAD="$(echo /usr/lib/*/libjemalloc.so.2)" +fi + +exec "${@}" diff --git a/lessons/220/ruby-app/bin/iodine b/lessons/220/ruby-app/bin/iodine new file mode 100755 index 000000000..70c6b50da --- /dev/null +++ b/lessons/220/ruby-app/bin/iodine @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'iodine' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +bundle_binstub = File.expand_path('bundle', __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?('This file was generated by Bundler') + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('rage-iodine', 'iodine') diff --git a/lessons/220/ruby-app/bin/rake b/lessons/220/ruby-app/bin/rake new file mode 100755 index 000000000..1ffe17657 --- /dev/null +++ b/lessons/220/ruby-app/bin/rake @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rake' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +ENV['RAKE_ENV'] ||= 'production' + +bundle_binstub = File.expand_path('bundle', __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?('This file was generated by Bundler') + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('rake', 'rake') diff --git a/lessons/220/ruby-app/config.ru b/lessons/220/ruby-app/config.ru new file mode 100644 index 000000000..730bd7ec1 --- /dev/null +++ b/lessons/220/ruby-app/config.ru @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'connection_pool' +require 'iodine' +require 'oj' +require 'pg' +require 'prometheus/middleware/exporter' +require 'rack' +require 'yaml' + +require_relative 'src/app' + +RubyVM::YJIT.enable +Iodine.threads = ENV.fetch('RAILS_MAX_THREADS', 2).to_i +Iodine.workers = ENV.fetch('WORKERS_NUM', 2).to_i +Oj.default_options = { mode: :compat } +pg_conf = YAML.load(File.read('db/config.yml'), aliases: true)['production'] +PG_POOL = ConnectionPool.new(size: Iodine.threads * Iodine.workers, timeout: 5) do + PG.connect(pg_conf['host'], pg_conf['port'], nil, nil, pg_conf['database'], pg_conf['username'], pg_conf['password']) +end + +# Enable if http metrics are required +# use Prometheus::Middleware::Collector +use Prometheus::Middleware::Exporter + +run App.new diff --git a/lessons/220/ruby-app/db/config.yml b/lessons/220/ruby-app/db/config.yml new file mode 100644 index 000000000..01535dbc2 --- /dev/null +++ b/lessons/220/ruby-app/db/config.yml @@ -0,0 +1,13 @@ +--- +default: &default + adapter: postgresql + host: localhost + database: putratest + username: appodeal + password: pass + +development: + <<: *default + +production: + <<: *default diff --git a/lessons/220/ruby-app/db/migrate/20241028151432_create_devices.rb b/lessons/220/ruby-app/db/migrate/20241028151432_create_devices.rb new file mode 100644 index 000000000..0dc11ef0d --- /dev/null +++ b/lessons/220/ruby-app/db/migrate/20241028151432_create_devices.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CreateDevices < ActiveRecord::Migration[7.2] + def change + create_table :devices do |t| + t.string :uuid + t.string :mac + t.string :firmware + + t.timestamps + end + end +end diff --git a/lessons/220/ruby-app/db/schema.rb b/lessons/220/ruby-app/db/schema.rb new file mode 100644 index 000000000..b6d7026fc --- /dev/null +++ b/lessons/220/ruby-app/db/schema.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.0].define(version: 20_241_028_151_432) do + # These are extensions that must be enabled in order to support this database + enable_extension 'pg_catalog.plpgsql' + + create_table 'devices', force: :cascade do |t| + t.string 'uuid' + t.string 'mac' + t.string 'firmware' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + end +end diff --git a/lessons/220/ruby-app/db/seeds.rb b/lessons/220/ruby-app/db/seeds.rb new file mode 100644 index 000000000..0f162112e --- /dev/null +++ b/lessons/220/ruby-app/db/seeds.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end diff --git a/lessons/220/ruby-app/src/app.rb b/lessons/220/ruby-app/src/app.rb new file mode 100644 index 000000000..6d922ad63 --- /dev/null +++ b/lessons/220/ruby-app/src/app.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative 'device' + +class App + def call(env) + case env['PATH_INFO'] + when '/api/devices' + if env['REQUEST_METHOD'] == 'GET' + response(Device.list, 200) + else + begin + device = Device.create(Oj.load(env['rack.input'].read)) + + response(device.to_h, 201) + rescue StandardError => e + response({ 'message' => e.message }, 400) + end + end + when '/up' + response({ 'message' => 'OK' }, 200) + else + response({ 'message' => 'Not Found' }, 404) + end + end + + def response(data, status) + [status, { 'Content-Type' => 'application/json' }, [Oj.dump(data)]] + end +end diff --git a/lessons/220/ruby-app/src/device.rb b/lessons/220/ruby-app/src/device.rb new file mode 100644 index 000000000..2a95b7c56 --- /dev/null +++ b/lessons/220/ruby-app/src/device.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'securerandom' + +require_relative 'prometheus_client' + +Device = Struct.new(:id, :uuid, :mac, :firmware, :created_at, :updated_at, keyword_init: true) do + def self.list + [ + { 'id' => 1, 'uuid' => '9add349c-c35c-4d32-ab0f-53da1ba40a2a', 'mac' => '5F-33-CC-1F-43-82', 'firmware' => '2.1.6', 'created_at' => '2024-5-28T15:21:51.137Z', 'updated_at' => '2024-5-28T15:21:51.137Z' }, + { 'id' => 2, 'uuid' => 'd2293412-36eb-46e7-9231-af7e9249fffe', 'mac' => 'E7-34-96-33-0C-4C', 'firmware' => '1.0.3', 'created_at' => '2024-01-28T15:20:51.137Z', 'updated_at' => '2024-01-28T15:20:51.137Z' }, + { 'id' => 3, 'uuid' => 'eee58ca8-ca51-47a5-ab48-163fd0e44b77', 'mac' => '68-93-9B-B5-33-B9', 'firmware' => '4.3.1', 'created_at' => '2024-8-28T15:18:21.137Z', 'updated_at' => '2024-8-28T15:18:21.137Z' } + ] + end + + def self.create(params) + now = Time.now + uuid = SecureRandom.uuid + device = new(uuid: uuid, mac: params['mac'], firmware: params['firmware'], created_at: now, updated_at: now) + + sql = <<~SQL + INSERT INTO "devices" ("uuid", "mac", "firmware", "created_at", "updated_at") + VALUES ('#{device.uuid}', '#{device.mac}', '#{device.firmware}', '#{device.created_at}', '#{device.updated_at}') + RETURNING "id" + SQL + + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + PG_POOL.with do |conn| + device.id = conn.exec(sql).first['id'] + end + duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + PrometheusClient.create_device_histogram.observe(duration, labels: { op: 'db' }) + + device + end +end diff --git a/lessons/220/ruby-app/src/prometheus_client.rb b/lessons/220/ruby-app/src/prometheus_client.rb new file mode 100644 index 000000000..e925a6b08 --- /dev/null +++ b/lessons/220/ruby-app/src/prometheus_client.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'prometheus/client' + +class PrometheusClient + BUCKETS = [ + 0.00001, 0.000015, 0.00002, 0.000025, 0.00003, 0.000035, 0.00004, 0.000045, + 0.00005, 0.000055, 0.00006, 0.000065, 0.00007, 0.000075, 0.00008, 0.000085, + 0.00009, 0.000095, 0.0001, 0.000101, 0.000102, 0.000103, 0.000104, 0.000105, + 0.000106, 0.000107, 0.000108, 0.000109, 0.00011, 0.000111, 0.000112, 0.000113, + 0.000114, 0.000115, 0.000116, 0.000117, 0.000118, 0.000119, 0.00012, 0.000121, + 0.000122, 0.000123, 0.000124, 0.000125, 0.000126, 0.000127, 0.000128, + 0.000129, 0.00013, 0.000131, 0.000132, 0.000133, 0.000134, 0.000135, 0.000136, + 0.000137, 0.000138, 0.000139, 0.00014, 0.000141, 0.000142, 0.000143, 0.000144, + 0.000145, 0.000146, 0.000147, 0.000148, 0.000149, 0.00015, 0.000151, 0.000152, + 0.000153, 0.000154, 0.000155, 0.000156, 0.000157, 0.000158, 0.000159, 0.00016, + 0.000161, 0.000162, 0.000163, 0.000164, 0.000165, 0.000166, 0.000167, + 0.000168, 0.000169, 0.00017, 0.000171, 0.000172, 0.000173, 0.000174, 0.000175, + 0.000176, 0.000177, 0.000178, 0.000179, 0.00018, 0.000181, 0.000182, 0.000183, + 0.000184, 0.000185, 0.000186, 0.000187, 0.000188, 0.000189, 0.00019, 0.000191, + 0.000192, 0.000193, 0.000194, 0.000195, 0.000196, 0.000197, 0.000198, + 0.000199, 0.0002, 0.00021, 0.00022, 0.00023, 0.00024, 0.00025, 0.00026, + 0.00027, 0.00028, 0.00029, 0.0003, 0.00031, 0.00032, 0.00033, 0.00034, + 0.00035, 0.00036, 0.00037, 0.00038, 0.00039, 0.0004, 0.00041, 0.00042, + 0.00043, 0.00044, 0.00045, 0.00046, 0.00047, 0.00048, 0.00049, 0.0005, + 0.00051, 0.00052, 0.00053, 0.00054, 0.00055, 0.00056, 0.00057, 0.00058, + 0.00059, 0.0006, 0.00061, 0.00062, 0.00063, 0.00064, 0.00065, 0.00066, + 0.00067, 0.00068, 0.00069, 0.0007, 0.00071, 0.00072, 0.00073, 0.00074, + 0.00075, 0.00076, 0.00077, 0.00078, 0.00079, 0.0008, 0.00081, 0.00082, + 0.00083, 0.00084, 0.00085, 0.00086, 0.00087, 0.00088, 0.00089, 0.0009, + 0.00091, 0.00092, 0.00093, 0.00094, 0.00095, 0.00096, 0.00097, 0.00098, + 0.00099, 0.001, 0.0015, 0.002, 0.0025, 0.003, 0.0035, 0.004, 0.0045, 0.005, + 0.0055, 0.006, 0.0065, 0.007, 0.0075, 0.008, 0.0085, 0.009, 0.0095, 0.01, + 0.015, 0.02, 0.025, 0.03, 0.035, 0.04, 0.045, 0.05, 0.055, 0.06, 0.065, 0.07, + 0.075, 0.08, 0.085, 0.09, 0.095, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, + 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0, 1.5, 2.0, 2.5, + 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0, + 10.5, 11.0, 11.5, 12.0, 12.5, 13.0, 13.5, 14.0, 14.5, 15.0, 15.5, 16.0, 16.5, 17.0, 17.5 + ].freeze + + class << self + def client + @client ||= Prometheus::Client.registry + end + + def create_device_histogram + @create_device_histogram ||= client.histogram( + :myapp_request_duration_seconds, docstring: 'Duration of the request.', labels: [:op], buckets: BUCKETS + ) + end + end +end diff --git a/lessons/220/ruby-app/tmp/.keep b/lessons/220/ruby-app/tmp/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/lessons/220/ruby-app/wrk.lua b/lessons/220/ruby-app/wrk.lua new file mode 100644 index 000000000..7a3f223cf --- /dev/null +++ b/lessons/220/ruby-app/wrk.lua @@ -0,0 +1,3 @@ +wrk.method = "POST" +wrk.headers["Content-Type"] = "application/json" +wrk.body = '{"mac":"5F-33-CC-1F-43-82", "firmware":"2.1.6"}'