lepus 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/Gemfile.lock +3 -3
- data/README.md +104 -9
- data/bin/web-demo +592 -0
- data/docs/brand/BRAND.md +67 -0
- data/docs/brand/apple-touch-icon.png +0 -0
- data/docs/brand/favicon-16.png +0 -0
- data/docs/brand/favicon-32.png +0 -0
- data/docs/brand/favicon.ico +0 -0
- data/docs/brand/favicon.svg +1 -0
- data/docs/brand/icon-512.png +0 -0
- data/docs/brand/lockup-lepus-mono.svg +1 -0
- data/docs/brand/lockup-lepus-on-dark.svg +1 -0
- data/docs/brand/lockup-lepus-on-light.svg +1 -0
- data/docs/brand/lockup-lepus-saga-mono.svg +1 -0
- data/docs/brand/lockup-lepus-saga-on-dark.svg +1 -0
- data/docs/brand/lockup-lepus-saga-on-light.svg +1 -0
- data/docs/brand/lockup-lepus-saga.svg +1 -0
- data/docs/brand/lockup-lepus.svg +1 -0
- data/docs/brand/mark-mono.svg +1 -0
- data/docs/brand/mark-on-dark.svg +1 -0
- data/docs/brand/mark-on-light.svg +1 -0
- data/docs/brand/mark-small-mono.svg +1 -0
- data/docs/brand/mark-small-on-dark.svg +1 -0
- data/docs/brand/mark-small-on-light.svg +1 -0
- data/docs/brand/mark-small.svg +1 -0
- data/docs/brand/mark.svg +1 -0
- data/docs/configuration.md +1 -0
- data/docs/consumers.md +39 -5
- data/docs/getting-started.md +1 -1
- data/docs/images/lepus-web.png +0 -0
- data/docs/middleware.md +1 -1
- data/docs/producers.md +41 -0
- data/docs/testing.md +67 -48
- data/docs/web.md +39 -13
- data/gemfiles/Gemfile.rails-5.2.lock +2 -2
- data/gemfiles/Gemfile.rails-6.1.lock +4 -4
- data/gemfiles/Gemfile.rails-7.2.lock +1 -1
- data/gemfiles/Gemfile.rails-8.0.lock +1 -1
- data/lib/lepus/configuration.rb +6 -0
- data/lib/lepus/consumer.rb +34 -0
- data/lib/lepus/consumers/config.rb +8 -7
- data/lib/lepus/consumers/stats.rb +15 -2
- data/lib/lepus/delay.rb +180 -0
- data/lib/lepus/primitive/string.rb +2 -12
- data/lib/lepus/producer.rb +6 -1
- data/lib/lepus/producers/definition.rb +3 -1
- data/lib/lepus/publisher.rb +29 -3
- data/lib/lepus/testing/rspec_matchers.rb +20 -1
- data/lib/lepus/testing.rb +2 -0
- data/lib/lepus/version.rb +1 -1
- data/lib/lepus/web/api.rb +45 -0
- data/lib/lepus/web/app.rb +19 -16
- data/lib/lepus/web/management_api.rb +125 -2
- data/lib/lepus/web/topology.rb +341 -0
- data/lib/lepus/web.rb +17 -18
- data/lib/lepus.rb +1 -0
- data/web/assets/app.css +862 -0
- data/web/assets/apple-touch-icon.png +0 -0
- data/web/assets/favicon-32.png +0 -0
- data/web/assets/favicon.svg +1 -0
- data/web/assets/fonts/overpass/OFL.txt +93 -0
- data/web/assets/fonts/overpass/overpass-latin-wght.woff2 +0 -0
- data/web/assets/js/api.js +27 -0
- data/web/assets/js/dom.js +95 -0
- data/web/assets/js/format.js +63 -0
- data/web/assets/js/live.js +74 -0
- data/web/assets/js/main.js +156 -0
- data/web/assets/js/map.js +606 -0
- data/web/assets/js/model.js +262 -0
- data/web/assets/js/rates.js +161 -0
- data/web/assets/js/theme.js +8 -0
- data/web/assets/js/views/connections.js +61 -0
- data/web/assets/js/views/overview.js +229 -0
- data/web/assets/js/views/processes.js +101 -0
- data/web/assets/js/views/queues.js +456 -0
- data/web/index.html +58 -153
- data/web/sw.js +14 -143
- metadata +47 -12
- data/test_offline.html +0 -189
- data/web/assets/css/styles.css +0 -635
- data/web/assets/js/app.js +0 -6
- data/web/assets/js/bootstrap.js +0 -20
- data/web/assets/js/controllers/connection_controller.js +0 -44
- data/web/assets/js/controllers/dashboard_controller.js +0 -499
- data/web/assets/js/controllers/queue_controller.js +0 -17
- data/web/assets/js/controllers/theme_controller.js +0 -31
- data/web/assets/js/offline-manager.js +0 -233
- data/web/assets/js/service-worker-manager.js +0 -65
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 259.67 84.00" width="278" height="90" role="img" aria-label="Lepus"><title>Lepus</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#e3e9f3" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#e3e9f3" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 259.67 84.00" width="278" height="90" role="img" aria-label="Lepus"><title>Lepus</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#1a2233" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#1a2233" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="currentColor"/></g><path fill="currentColor" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path fill="currentColor" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="currentColor"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#e3e9f3" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#e3e9f3" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path fill="#e3e9f3" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#1a2233" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#1a2233" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path fill="#1a2233" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><g transform="translate(-13.03 -60.26) scale(1.2414)"><path class="i" d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path class="f" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path class="f" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 259.67 84.00" width="278" height="90" role="img" aria-label="Lepus"><title>Lepus</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><g transform="translate(-13.03 -60.26) scale(1.2414)"><path class="i" d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path class="f" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="currentColor"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#e3e9f3" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#1a2233" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke="currentColor" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="currentColor"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke="#e3e9f3" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke="#1a2233" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><path class="i" d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="#f2a30f"/></svg>
|
data/docs/brand/mark.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><path class="i" d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></svg>
|
data/docs/configuration.md
CHANGED
|
@@ -27,6 +27,7 @@ end
|
|
|
27
27
|
| `connection_name` | gem-generated | Shown in RabbitMQ management UI — set to your service name |
|
|
28
28
|
| `recovery_attempts` | `10` | Max automatic reconnects; `nil` for infinite |
|
|
29
29
|
| `recover_from_connection_close` | `true` | Auto-recover after a clean close |
|
|
30
|
+
| `delay_queue_expires` | `3_600_000` | Milliseconds an unused delay queue is kept beyond its delay (`x-expires`); `nil` disables. See [Delayed messages](producers.md#delayed-messages) |
|
|
30
31
|
|
|
31
32
|
### Named connections
|
|
32
33
|
|
data/docs/consumers.md
CHANGED
|
@@ -42,8 +42,9 @@ configure(
|
|
|
42
42
|
| `exchange` | Bind to this exchange. String, or hash (`name`, `type`, `durable`, `auto_delete`). |
|
|
43
43
|
| `routing_key` | Binding routing key(s). |
|
|
44
44
|
| `prefetch` | Channel QoS (`basic.qos`). Leave at `1` unless you know you need more. |
|
|
45
|
-
| `retry_queue` | When truthy,
|
|
45
|
+
| `retry_queue` | When truthy, declares a `<queue>.retry` queue and dead-letters rejected messages into it; after `delay` ms they return to the queue. Only `:reject` (and a `perform` that raises, which rejects) goes there; `:requeue` and `:nack` put the message straight back on the queue. |
|
|
46
46
|
| `error_queue` | Queue name (string) or `true` to auto-name. Messages that exhaust retries land here. |
|
|
47
|
+
| `delays` | Only delays (ms) accepted by `delay!`; anything else raises `Lepus::Error`. See [Delaying a message](#delaying-a-message). |
|
|
47
48
|
| `process.name` | The named worker pool this consumer runs in — see `Lepus.configure.worker(:name)`. |
|
|
48
49
|
| `process.threads` | Threads inside the consumer's worker dedicated to this consumer. |
|
|
49
50
|
|
|
@@ -69,6 +70,7 @@ Return a disposition symbol or call one of the helpers:
|
|
|
69
70
|
| `:reject` | `reject!` | Reject without requeue (drops, or goes to DLX if configured). |
|
|
70
71
|
| `:requeue` | `requeue!` | Reject with requeue — back of the queue. |
|
|
71
72
|
| `:nack` | `nack!` | Negative-ack. |
|
|
73
|
+
| `:ack` | `delay!(ms)` | Re-deliver the message to this queue after `ms` milliseconds, then ack it. See [Delaying a message](#delaying-a-message). |
|
|
72
74
|
|
|
73
75
|
A `perform` that returns nothing implicitly acks. A `perform` that raises is logged, `on_thread_error` fires, and the message is rejected.
|
|
74
76
|
|
|
@@ -123,10 +125,42 @@ use :max_retry, retries: 5, error_queue: 'orders.error'
|
|
|
123
125
|
|
|
124
126
|
On rejection:
|
|
125
127
|
|
|
126
|
-
1.
|
|
127
|
-
2.
|
|
128
|
-
3.
|
|
129
|
-
4.
|
|
128
|
+
1. The rejected message is dead-lettered into the retry queue.
|
|
129
|
+
2. After the TTL, the retry queue dead-letters it back to the original queue for another attempt.
|
|
130
|
+
3. On each delivery `max_retry` counts the `rejected` entries of the `x-death` header.
|
|
131
|
+
4. Once the rejected count exceeds `retries` (the first attempt plus N retries), the middleware publishes the payload, without headers or properties, to the error queue and acks the message.
|
|
132
|
+
|
|
133
|
+
## Delaying a message
|
|
134
|
+
|
|
135
|
+
`delay!(ms)` (aliased `delay`) puts the message being processed back on this consumer's queue after `ms` milliseconds and acks the current delivery. Use it when the work can't be done yet, e.g. waiting for a replica to catch up:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
class ReindexConsumer < Lepus::Consumer
|
|
139
|
+
configure(queue: 'reindex', exchange: 'stories', routing_key: 'story.published')
|
|
140
|
+
use :json
|
|
141
|
+
|
|
142
|
+
MAX_DELAYS = 3
|
|
143
|
+
|
|
144
|
+
def perform(message)
|
|
145
|
+
attempts = message.metadata.headers.to_h['x-lepus-delayed-count'].to_i
|
|
146
|
+
return delay!(5_000) if replica_behind? && attempts < MAX_DELAYS
|
|
147
|
+
|
|
148
|
+
reindex(message.payload)
|
|
149
|
+
ack!
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
It accepts milliseconds or anything responding to `in_milliseconds` (`5.seconds`). No configuration is needed, but every distinct delay creates a queue, so you can list the allowed ones with `configure(..., delays: [5_000, 60_000])`. Any other value raises `Lepus::Error`, which rejects the message like any other error in `perform`.
|
|
155
|
+
|
|
156
|
+
- The message is re-published as received (raw payload, headers and properties, before middlewares changed it), minus `expiration`, through the default exchange to a `reindex.delay.5000` queue with `x-message-ttl: 5000` that dead-letters into `reindex`. The `.delay.` infix keeps it apart from the `<exchange>.delayed.<ms>` queues of producer delays, even when the queue and exchange share a name. Only this queue gets it again, not other queues bound to the exchange.
|
|
157
|
+
- The `x-lepus-delayed-count` header is incremented on each delay, so you can cap your own backoff.
|
|
158
|
+
- The redelivery arrives with routing key `reindex` (the queue name), like retries do. Put anything you route on in the payload or headers.
|
|
159
|
+
- The queue is declared on first use and gets `x-expires` like producer delays; see [Queue expiry](producers.md#queue-expiry).
|
|
160
|
+
- `max_retry` ignores delays: it only counts `rejected` deaths, while delay queues produce `expired` ones.
|
|
161
|
+
- `delay!` returns `:ack`, so the `:unique` middleware releases the dedupe lock under the default `release_on: [:ack]`. Duplicates published during the delay are not suppressed, and the redelivered message doesn't hold a lock.
|
|
162
|
+
|
|
163
|
+
`delay!` must be called from `perform`; outside a delivery it raises `Lepus::Error`. Any publish to the default exchange can be delayed the same way: `Lepus::Publisher.new('').publish(payload, routing_key: 'reindex', delay: 5_000)`.
|
|
130
164
|
|
|
131
165
|
## Worker assignment
|
|
132
166
|
|
data/docs/getting-started.md
CHANGED
|
@@ -57,7 +57,7 @@ The `perform` method returns a symbol indicating disposition:
|
|
|
57
57
|
- `:ack` — acknowledge; RabbitMQ removes the message.
|
|
58
58
|
- `:reject` — reject; message is dropped (or routed to a dead-letter exchange if configured).
|
|
59
59
|
- `:requeue` — reject and requeue for another delivery attempt.
|
|
60
|
-
- `:nack` — negative-ack
|
|
60
|
+
- `:nack` — negative-ack and requeue (like `:requeue`).
|
|
61
61
|
|
|
62
62
|
`ack!`, `reject!`, `requeue!`, `nack!` are also available as helper methods.
|
|
63
63
|
|
data/docs/images/lepus-web.png
CHANGED
|
Binary file
|
data/docs/middleware.md
CHANGED
|
@@ -77,7 +77,7 @@ Track retry count via RabbitMQ `x-death` headers and route to an error queue aft
|
|
|
77
77
|
use :max_retry, retries: 5, error_queue: 'orders.error'
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
Requires the consumer's `configure` to declare a `retry_queue:` so
|
|
80
|
+
Requires the consumer's `configure` to declare a `retry_queue:` so rejected messages (`:reject` or a raised error) loop through the retry queue.
|
|
81
81
|
|
|
82
82
|
### `:exception_logger`
|
|
83
83
|
|
data/docs/producers.md
CHANGED
|
@@ -40,6 +40,7 @@ configure(
|
|
|
40
40
|
| `exchange` | String (name only) or hash (`name`, `type`, `durable`, `auto_delete`). Auto-declared on first publish. |
|
|
41
41
|
| `publish` | Default publish options merged into every `publish` call: `persistent`, `mandatory`, `content_type`, `expiration`, etc. |
|
|
42
42
|
| `connection` | Named connection to publish through. Defaults to `:default` (`rabbitmq_url`). See [Named connections](#named-connections). |
|
|
43
|
+
| `delays` | Only delays (ms) accepted by `publish(..., delay:)`; anything else raises `Lepus::Error`. See [Delayed messages](#delayed-messages). |
|
|
43
44
|
|
|
44
45
|
## Publishing
|
|
45
46
|
|
|
@@ -60,6 +61,46 @@ All Bunny publish options are supported. Frequently useful:
|
|
|
60
61
|
| `content_type` | MIME type. Auto-set to `application/json` by `:json` middleware. |
|
|
61
62
|
| `expiration` | Per-message TTL in milliseconds. |
|
|
62
63
|
| `mandatory` | Return the message if it can't be routed. |
|
|
64
|
+
| `delay` | Deliver the message after this many milliseconds (Integer, Float or anything responding to `in_milliseconds`). Lepus option, never sent to Bunny. See [Delayed messages](#delayed-messages). |
|
|
65
|
+
|
|
66
|
+
## Delayed messages
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
OrdersProducer.publish(payload, routing_key: 'order.reminder', delay: 30_000)
|
|
70
|
+
OrdersProducer.publish(payload, routing_key: 'order.reminder', delay: 5.minutes) # ActiveSupport::Duration
|
|
71
|
+
Lepus::Publisher.new('orders').publish(payload, routing_key: 'order.reminder', delay: 30_000)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A default can be set with `configure(publish: { delay: 1_000 })`. Middlewares run as usual and don't see `delay` among the publish options.
|
|
75
|
+
|
|
76
|
+
### How it works
|
|
77
|
+
|
|
78
|
+
RabbitMQ has no delayed delivery without the `rabbitmq_delayed_message_exchange` plugin, so Lepus builds it from TTL queues and dead-lettering. For a target exchange `orders` and a delay of 30000 ms, Lepus declares on first use:
|
|
79
|
+
|
|
80
|
+
| Resource | Details |
|
|
81
|
+
|----------|---------|
|
|
82
|
+
| `orders.delayed` | Durable headers exchange. Delayed messages are published here with the original routing key plus the `lepus-delay: 30000` header. |
|
|
83
|
+
| `orders.delayed.30000` | Durable queue bound to `orders.delayed` with `x-match: all, lepus-delay: 30000`. Arguments: `x-message-ttl: 30000`, `x-dead-letter-exchange: orders`, `x-expires` (see below). |
|
|
84
|
+
|
|
85
|
+
When the TTL expires RabbitMQ dead-letters the message into `orders`. There's no `x-dead-letter-routing-key`, so it keeps its original routing key and headers, which works for topic, direct, fanout and headers exchanges. The delivered message also carries the `lepus-delay` header and RabbitMQ's `x-death` header.
|
|
86
|
+
|
|
87
|
+
The delay header has no `x-` prefix on purpose: with `x-match: all`, RabbitMQ ignores `x-` binding arguments, so every delay queue would receive every message.
|
|
88
|
+
|
|
89
|
+
Each delay value has its own queue with a per-queue TTL, so there's no head-of-line blocking: a 1 second delay published after a 1 hour delay still arrives after 1 second. Every distinct value creates a queue, so prefer a small set of fixed delays (e.g. 1s, 30s, 5min) over computed ones. Configure the producer with `delays: [1_000, 30_000, 300_000]` to reject any other value with `Lepus::Error` before publishing; Lepus also logs at info level the first time each process declares a delay queue.
|
|
90
|
+
|
|
91
|
+
Lepus doesn't use per-message `expiration` for delays, but one you pass is still applied inside the delay queue: RabbitMQ uses the lower of the two TTLs, so `expiration: 10_000, delay: 60_000` arrives after 10 seconds, and it drops `expiration` when dead-lettering, so the delivered message no longer has it.
|
|
92
|
+
|
|
93
|
+
### Queue expiry
|
|
94
|
+
|
|
95
|
+
Delay queues get `x-expires` set to the delay plus `config.delay_queue_expires` (default 1 hour), so queues for delays you no longer use are deleted by RabbitMQ, together with any message still in them. Publishing doesn't count as queue usage, so while a process keeps publishing a delay, Lepus re-declares its queue at least every `delay_queue_expires / 2` (and at least once a minute). A message published at any time therefore expires before its queue does. Set `config.delay_queue_expires = nil` to keep delay queues forever.
|
|
96
|
+
|
|
97
|
+
Queue arguments can't change once a queue exists. If you change `delay_queue_expires`, publishing to an existing delay queue raises `Lepus::Error` naming the queue. Keep the previous value, or delete the queue (it only holds messages still waiting) and restart every process that publishes delays in the same deploy: until a process re-declares a deleted queue (up to a minute later), its delayed messages for that queue are dropped.
|
|
98
|
+
|
|
99
|
+
### Declarations
|
|
100
|
+
|
|
101
|
+
Declarations are memoized per connection, delay queue and arguments, and run on a short-lived channel, so a failure doesn't close the channel used for publishing.
|
|
102
|
+
|
|
103
|
+
`mandatory` and publisher confirms only cover the publish into the delay exchange or queue. Dead-lettering into the target when the TTL expires is not subject to them: if the target exchange doesn't exist, or no binding matches the routing key at that moment, RabbitMQ drops the message without a return or nack. Publishing through the producer declares the exchange, but the bindings must also be in place when the delay ends.
|
|
63
104
|
|
|
64
105
|
## Payload types
|
|
65
106
|
|
data/docs/testing.md
CHANGED
|
@@ -9,14 +9,18 @@ Lepus ships a test-mode module that captures publishes and runs consumer `perfor
|
|
|
9
9
|
require 'lepus/testing'
|
|
10
10
|
|
|
11
11
|
RSpec.configure do |config|
|
|
12
|
-
config.before(:each)
|
|
13
|
-
|
|
12
|
+
config.before(:each) do
|
|
13
|
+
Lepus::Testing.enable!
|
|
14
|
+
Lepus::Testing.clear_all_messages!
|
|
15
|
+
end
|
|
16
|
+
config.after(:each) { Lepus::Testing.disable! }
|
|
14
17
|
end
|
|
15
18
|
```
|
|
16
19
|
|
|
17
20
|
Once enabled:
|
|
18
21
|
|
|
19
|
-
- Publishes don't hit RabbitMQ. They're captured in
|
|
22
|
+
- Publishes don't hit RabbitMQ. They're captured in in-memory fake exchanges keyed by exchange name.
|
|
23
|
+
- Consumer exceptions are re-raised instead of turning into `:reject`, so failures show up in your specs. Use `Lepus::Testing.consumer_capture_errors!` to get the production behavior back.
|
|
20
24
|
- Consumer handling is synchronous when invoked through `Lepus::Testing.consumer_perform`.
|
|
21
25
|
|
|
22
26
|
## Testing a consumer
|
|
@@ -24,10 +28,7 @@ Once enabled:
|
|
|
24
28
|
```ruby
|
|
25
29
|
describe OrdersConsumer do
|
|
26
30
|
it 'creates an order' do
|
|
27
|
-
result = Lepus::Testing.consumer_perform(
|
|
28
|
-
OrdersConsumer,
|
|
29
|
-
{ order_id: 42, total: 99.99 }
|
|
30
|
-
)
|
|
31
|
+
result = Lepus::Testing.consumer_perform(OrdersConsumer, { order_id: 42, total: 99.99 })
|
|
31
32
|
|
|
32
33
|
expect(result).to eq(:ack)
|
|
33
34
|
expect(Order.find(42).total).to eq(99.99)
|
|
@@ -41,27 +42,24 @@ describe OrdersConsumer do
|
|
|
41
42
|
it 'sets delivery info and metadata' do
|
|
42
43
|
result = Lepus::Testing.consumer_perform(
|
|
43
44
|
OrdersConsumer,
|
|
44
|
-
{ order_id: 7 },
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
payload: { order_id: 7 },
|
|
46
|
+
routing_key: 'order.created',
|
|
47
|
+
correlation_id: 'abc-123',
|
|
48
|
+
headers: { 'tenant' => 'acme' }
|
|
47
49
|
)
|
|
48
50
|
expect(result).to eq(:ack)
|
|
49
51
|
end
|
|
50
52
|
end
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
`consumer_perform`
|
|
55
|
+
`consumer_perform(ConsumerClass, message_or_payload)` accepts:
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
delivery_info: {},
|
|
60
|
-
metadata: {}
|
|
61
|
-
)
|
|
62
|
-
```
|
|
57
|
+
- a Hash, used as a JSON payload;
|
|
58
|
+
- a String, used as a `text/plain` payload;
|
|
59
|
+
- a Hash with a `:payload` key, where every other key maps to a `MessageBuilder#with_<key>` method (`routing_key`, `exchange`, `headers`, `correlation_id`, `content_type`, `redelivered`, …);
|
|
60
|
+
- a `Lepus::Message`, e.g. built with `Lepus::Testing.message_builder`.
|
|
63
61
|
|
|
64
|
-
It
|
|
62
|
+
It runs the full middleware chain (including global middlewares) and returns the disposition symbol.
|
|
65
63
|
|
|
66
64
|
## Testing a producer
|
|
67
65
|
|
|
@@ -71,23 +69,21 @@ describe OrdersProducer do
|
|
|
71
69
|
order = Order.create!(id: 42, total: 99.99)
|
|
72
70
|
OrdersProducer.order_created(order)
|
|
73
71
|
|
|
74
|
-
messages = Lepus::Testing.producer_messages(OrdersProducer)
|
|
72
|
+
messages = OrdersProducer.messages # same as Lepus::Testing.producer_messages(OrdersProducer)
|
|
75
73
|
expect(messages.size).to eq(1)
|
|
76
|
-
expect(messages[0][:payload]).to include(order_id: 42)
|
|
77
74
|
expect(messages[0][:routing_key]).to eq('order.created')
|
|
78
75
|
end
|
|
79
|
-
|
|
80
|
-
it 'runs through middleware' do
|
|
81
|
-
OrdersProducer.publish({ foo: 'bar' }, routing_key: 'x')
|
|
82
|
-
|
|
83
|
-
msg = Lepus::Testing.producer_messages(OrdersProducer).last
|
|
84
|
-
expect(msg[:metadata][:correlation_id]).to be_present # set by :correlation_id middleware
|
|
85
|
-
expect(msg[:metadata][:content_type]).to eq('application/json')
|
|
86
|
-
end
|
|
87
76
|
end
|
|
88
77
|
```
|
|
89
78
|
|
|
90
|
-
|
|
79
|
+
Each captured message is a hash with `:exchange`, `:connection`, `:payload`, `:routing_key`, `:headers`, `:persistent`, `:mandatory`, `:immediate`, `:content_type`, `:delay`, `:expiration` and `:timestamp` (when it was captured). `:payload` is what reached the publisher, so with the `:json` middleware it's the serialized JSON string.
|
|
80
|
+
|
|
81
|
+
Query a fake exchange directly:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Lepus::Testing.exchange('orders').messages
|
|
85
|
+
Lepus::Testing.exchange('orders').find_messages(routing_key: 'order.created', headers: { 'tenant' => 'acme' })
|
|
86
|
+
```
|
|
91
87
|
|
|
92
88
|
Each fake message also records the named connection it was published through:
|
|
93
89
|
|
|
@@ -97,22 +93,51 @@ Lepus::Testing.exchange('orders').find_messages(connection: :replica)
|
|
|
97
93
|
|
|
98
94
|
The connection may be given as a symbol or a string. Publishing through a connection that was never registered with `config.connection` raises `Lepus::InvalidConnectionError`, as it does outside of fake mode.
|
|
99
95
|
|
|
100
|
-
|
|
96
|
+
### Delayed messages
|
|
97
|
+
|
|
98
|
+
`delay:` is recorded on the target exchange, normalized to milliseconds, so `OrdersProducer.messages` and `to_exchange` keep working:
|
|
101
99
|
|
|
102
100
|
```ruby
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
OrdersProducer.publish({ order_id: 42 }, routing_key: 'order.reminder', delay: 30_000)
|
|
102
|
+
|
|
103
|
+
expect(OrdersProducer.messages.last[:delay]).to eq(30_000)
|
|
105
104
|
```
|
|
106
105
|
|
|
107
|
-
|
|
106
|
+
A consumer's `delay!(ms)` is recorded on the default exchange (`''`) with the consumer queue as routing key, and the `x-lepus-delayed-count` header incremented:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
Lepus::Testing.consumer_perform(ReindexConsumer, { story_id: 1 })
|
|
110
|
+
|
|
111
|
+
message = Lepus::Testing.exchange('').messages.last
|
|
112
|
+
expect(message).to include(routing_key: 'reindex', delay: 5_000)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## RSpec matchers
|
|
116
|
+
|
|
117
|
+
The matchers are included in RSpec when `lepus/testing` is required.
|
|
108
118
|
|
|
109
119
|
```ruby
|
|
110
120
|
expect { OrdersProducer.order_created(order) }
|
|
111
|
-
.to
|
|
112
|
-
.
|
|
113
|
-
.
|
|
121
|
+
.to lepus_publish_message
|
|
122
|
+
.to_exchange('orders')
|
|
123
|
+
.with_routing_key('order.created')
|
|
124
|
+
.with(satisfy { |json| MultiJson.load(json)['order_id'] == order.id }) # :json middleware serialized it
|
|
125
|
+
|
|
126
|
+
expect { OrdersProducer.publish(payload, delay: 30_000) }
|
|
127
|
+
.to lepus_publish_message(1).with_delay(30_000)
|
|
128
|
+
|
|
129
|
+
# Matches the messages already captured for a producer
|
|
130
|
+
expect(OrdersProducer).to lepus_publish_message.with_routing_key('order.created')
|
|
131
|
+
|
|
132
|
+
# Consumer dispositions
|
|
133
|
+
expect(OrdersConsumer).to lepus_acknowledge_message({ order_id: 42 }) # alias: lepus_ack_message
|
|
134
|
+
expect(OrdersConsumer).to lepus_reject_message({ bad: 'data' })
|
|
135
|
+
expect(OrdersConsumer).to lepus_requeue_message(payload)
|
|
136
|
+
expect(OrdersConsumer).to lepus_nack_message(payload)
|
|
114
137
|
```
|
|
115
138
|
|
|
139
|
+
`with_delay` accepts milliseconds or anything responding to `in_milliseconds`. `with` takes a value or any RSpec matcher and is checked against the captured `:payload`.
|
|
140
|
+
|
|
116
141
|
## Testing middleware in isolation
|
|
117
142
|
|
|
118
143
|
Middlewares are plain Ruby objects with a `call(message, app)` method. Unit-test them directly:
|
|
@@ -121,8 +146,8 @@ Middlewares are plain Ruby objects with a `call(message, app)` method. Unit-test
|
|
|
121
146
|
describe LogLevelMiddleware do
|
|
122
147
|
it 'logs before calling down the chain' do
|
|
123
148
|
middleware = LogLevelMiddleware.new(level: :debug)
|
|
124
|
-
message
|
|
125
|
-
captured
|
|
149
|
+
message = Lepus::Testing.message_builder.with_payload({ x: 1 }).with_routing_key('order.created').build
|
|
150
|
+
captured = nil
|
|
126
151
|
|
|
127
152
|
allow(Lepus.logger).to receive(:debug) { |msg| captured = msg }
|
|
128
153
|
middleware.call(message, ->(m) { :ack })
|
|
@@ -132,17 +157,11 @@ describe LogLevelMiddleware do
|
|
|
132
157
|
end
|
|
133
158
|
```
|
|
134
159
|
|
|
135
|
-
`Lepus::Testing::MessageBuilder
|
|
160
|
+
`Lepus::Testing.message_builder` returns a `Lepus::Testing::MessageBuilder`; chain `with_*` methods (`with_payload`, `with_routing_key`, `with_headers`, `with_correlation_id`, `with_channel`, …) and call `build` to get a `Lepus::Message`.
|
|
136
161
|
|
|
137
162
|
## Resetting between tests
|
|
138
163
|
|
|
139
|
-
`Lepus::Testing.
|
|
140
|
-
|
|
141
|
-
```ruby
|
|
142
|
-
RSpec.configure do |config|
|
|
143
|
-
config.before(:each) { Lepus::Testing.enable!; Lepus::Testing.reset! }
|
|
144
|
-
end
|
|
145
|
-
```
|
|
164
|
+
`Lepus::Testing.clear_all_messages!` clears captured publishes. `Lepus::Testing.disable!` turns fake publishing and error re-raising off.
|
|
146
165
|
|
|
147
166
|
## When you do need a real RabbitMQ
|
|
148
167
|
|
data/docs/web.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
Lepus ships a Rack-based monitoring UI
|
|
5
|
+
Lepus ships a Rack-based monitoring UI: a live map of how messages move from exchanges through queues to consumers, the broker's message rates, and the supervisors, workers, queues and connections behind them.
|
|
6
6
|
|
|
7
7
|
## Running it standalone
|
|
8
8
|
|
|
@@ -19,7 +19,7 @@ Visit http://localhost:9292.
|
|
|
19
19
|
require 'lepus/web'
|
|
20
20
|
|
|
21
21
|
authenticate :user, ->(u) { u.admin? } do
|
|
22
|
-
mount Lepus::Web
|
|
22
|
+
mount Lepus::Web => '/lepus'
|
|
23
23
|
end
|
|
24
24
|
```
|
|
25
25
|
|
|
@@ -27,7 +27,7 @@ Or with Devise:
|
|
|
27
27
|
|
|
28
28
|
```ruby
|
|
29
29
|
authenticate :admin_user do
|
|
30
|
-
mount Lepus::Web
|
|
30
|
+
mount Lepus::Web => '/admin/lepus'
|
|
31
31
|
end
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -35,11 +35,34 @@ end
|
|
|
35
35
|
|
|
36
36
|
## What it shows
|
|
37
37
|
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
38
|
+
- **Topology map.** Each exchange is a band of lines, one per bound queue, running exchange → queue → consumer → acknowledged. Amber tokens ride each segment at a pace that grows with its message rate (on a log scale, capped), a hollow amber bar is a backlog piling up at its queue, violet loops under a line are messages waiting in the retry queue and red bars count messages parked in the error queue. Messages delayed on purpose (see below) wait in a teal clock pill tucked under the queue they will be released to, or under the exchange name when they are released to the exchange; they are never counted as backlog or retries. Quiet queues, and anything past the busiest few per exchange, fold into a "+N" row you can expand. Point at or focus a queue for exact numbers; select it to find it in the queues table.
|
|
39
|
+
- **Headline.** Messages flowing, queues backing up (growing, or with nobody reading them), messages retrying, messages delayed (when there are any) and messages in error queues, in one sentence that also serves as the map's key.
|
|
40
|
+
- **Message rates.** Published, delivered, acknowledged and redelivered per second across the broker over the last 10 minutes or hour, from the samples the management API keeps, with a table view.
|
|
41
|
+
- **Queues.** Every main queue under the exchange that routes to it, in the map's order, with each exchange's totals; a queue bound to several exchanges sits under the first and notes the others, and queues with no binding come last under "Default exchange", as on the map. Columns are ready (with how fast a growing backlog grows), unacked, consumers, published, delivered and acknowledged per second (on an exchange's row, Published/s is what reaches the exchange), and the messages in the queue's retry and error queues ("–" when it has none). Sorting orders the queues within each exchange. Expand a queue to see its retry, delay and error queues with their own counts and what their arguments do ("retries after 5 s, back to orders.billing", "releases after 5 s to orders.billing, expires when unused for 1 h"). A queue's row says how many messages wait in its delay queues and for how long ("340 delayed for 1 min and 1 h"); an exchange's delay queues are listed right under the exchange, and its totals include everything delayed for it; delay queues whose queue or exchange no longer exists are listed under "Default exchange". Idle queues are muted, and an exchange's idle queues fold into one row; on a broker with more than 30 queues each exchange shows its first six and folds the rest into "+N more queues". Selecting a queue on the map brings its row into view, unfolded, expanded and focused; a queue's name leads back to it on the map.
|
|
42
|
+
- **Processes.** Supervisors, their workers (PID, memory, connections, heartbeat health) and each consumer's processed, rejected and errored counts since the worker started. A consumer configured with `delays:` lists them, and messages it handed back with `delay!` count as delayed rather than processed.
|
|
43
|
+
- **Connections.** The broker's client connections.
|
|
44
|
+
|
|
45
|
+
Retry and error queues are recognised from what consumers report in their heartbeats, from their arguments (a TTL queue that dead-letters into a queue which dead-letters back is its retry queue; a queue another one dead-letters into is its error queue) and from the `.retry` / `.error` suffixes. A queue a consumer reads, or one bound to a named exchange, is always drawn as a main queue.
|
|
46
|
+
|
|
47
|
+
Delay queues are the ones Lepus declares for [delayed messages](../README.md#delayed-messages), recognised by their name and arguments together: `<queue>.delay.<ms>` with an `x-message-ttl` of `<ms>` dead-letters into `<queue>` through the default exchange (a `delay:` publish through the default exchange, or a consumer's `delay!`), and `<exchange>.delayed.<ms>` with an `x-message-ttl` of `<ms>` dead-letters into `<exchange>`, keeping the routing key, behind the `<exchange>.delayed` headers exchange (a `delay:` publish to any other exchange). The `<exchange>.delayed` exchange draws no band of its own. A delay queue RabbitMQ deleted after `x-expires` (`config.delay_queue_expires` past its delay) simply drops off the next poll.
|
|
48
|
+
|
|
49
|
+
## Refreshing
|
|
50
|
+
|
|
51
|
+
The page polls every 5 seconds by default. The **Live** control in the top bar pauses it or switches to 2, 5, 15 or 60 seconds, and the choice is remembered in the browser. Polling also stops while the tab is in the background, and tokens freeze while paused. The light/dark theme follows the system unless you pick one.
|
|
52
|
+
|
|
53
|
+
The page loads nothing from other origins (its font is bundled), uses no inline scripts or styles and is served with a strict `Content-Security-Policy`. Earlier versions installed a service worker for offline use; it now removes itself and its caches on the next visit.
|
|
54
|
+
|
|
55
|
+
## Trying it without RabbitMQ
|
|
56
|
+
|
|
57
|
+
From a checkout of the gem:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bin/web-demo # http://127.0.0.1:9393
|
|
61
|
+
HEAVY=1 bin/web-demo # ~70 queues on 12 exchanges, 20 workers, deep backlogs
|
|
62
|
+
MOUNT=/lepus bin/web-demo # served under a sub-path, like a Rails mount
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
It simulates a broker whose rates drift, with a backlog that builds and drains and a burst of errors every few minutes.
|
|
43
66
|
|
|
44
67
|
## Registry backend
|
|
45
68
|
|
|
@@ -66,13 +89,16 @@ Each worker heartbeats into the registry every `config.process_heartbeat_interva
|
|
|
66
89
|
|
|
67
90
|
## API endpoints
|
|
68
91
|
|
|
69
|
-
The dashboard
|
|
92
|
+
The dashboard is a single page reading a small read-only JSON API under `/api/...`. You can use it directly for custom dashboards or alerting:
|
|
70
93
|
|
|
71
|
-
|
|
94
|
+
- `GET /api/topology`: exchanges, bindings (with their routing keys), queues with their role (`main`, `retry`, `error`, `delay`) and parent, rates and backlog, message TTL, dead-letter exchange and routing key and `x-expires`, and the consumers of each queue (with their `delayed` count and `delays`) merged from the broker and worker heartbeats. A delay queue also has `delay_ms`, `target` and `target_type` (`queue` or `exchange`); every queue and exchange has `delayed`, the messages waiting to be released to it, and `delays`, the delays they wait; a `<exchange>.delayed` exchange has role `delay` and `releases_to`, and bindings carry the `delays` of their `lepus-delay` header. `management` is `false` (with `management_error`) when the management API did not answer; the topology is then what consumers declared.
|
|
95
|
+
- `GET /api/overview?age=600|3600`: broker-wide message rates, queue totals and rate history over the window.
|
|
96
|
+
- `GET /api/processes`: every process sending heartbeats, with per-consumer counts.
|
|
97
|
+
- `GET /api/queues`, `GET /api/exchanges`, `GET /api/connections`: the management API's lists.
|
|
98
|
+
- `GET /api/meta`: version, heartbeat settings and polling choices.
|
|
99
|
+
- `GET /api/health`
|
|
72
100
|
|
|
73
|
-
|
|
74
|
-
- `GET /api/consumers` — all registered consumers with counts
|
|
75
|
-
- `GET /api/queues` — queue metadata from the RabbitMQ management API (if `management_api_url` is set)
|
|
101
|
+
`/api/processes`, `/api/meta` and `/api/health` work without the RabbitMQ Management API. `/api/topology` degrades to what worker heartbeats declared when it is missing, and the rest need it (`config.management_api_url`, default `http://<rabbitmq host>:15672`, credentials and vhost taken from `rabbitmq_url`). When it fails, the dashboard only says whether it was unreachable, rejected the credentials or returned an error; the details go to the log.
|
|
76
102
|
|
|
77
103
|
## Prometheus metrics
|
|
78
104
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
lepus (0.
|
|
4
|
+
lepus (0.3.0)
|
|
5
5
|
base64
|
|
6
6
|
bunny
|
|
7
7
|
concurrent-ruby
|
|
@@ -160,7 +160,7 @@ GEM
|
|
|
160
160
|
thor (>= 0.19.0, < 2.0)
|
|
161
161
|
rainbow (3.1.1)
|
|
162
162
|
rake (13.4.2)
|
|
163
|
-
rbtree (0.4.
|
|
163
|
+
rbtree (0.4.7)
|
|
164
164
|
redis (5.4.1)
|
|
165
165
|
redis-client (>= 0.22.0)
|
|
166
166
|
redis-client (0.28.0)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
lepus (0.
|
|
4
|
+
lepus (0.3.0)
|
|
5
5
|
base64
|
|
6
6
|
bunny
|
|
7
7
|
concurrent-ruby
|
|
@@ -78,8 +78,8 @@ GEM
|
|
|
78
78
|
base64 (0.3.0)
|
|
79
79
|
bigdecimal (4.1.1)
|
|
80
80
|
builder (3.3.0)
|
|
81
|
-
bunny (3.
|
|
82
|
-
amq-protocol (~> 2.
|
|
81
|
+
bunny (3.2.0)
|
|
82
|
+
amq-protocol (~> 2.8)
|
|
83
83
|
logger (~> 1, >= 1.7)
|
|
84
84
|
sorted_set (~> 1, >= 1.0.2)
|
|
85
85
|
coderay (1.1.3)
|
|
@@ -179,7 +179,7 @@ GEM
|
|
|
179
179
|
thor (~> 1.0)
|
|
180
180
|
rainbow (3.1.1)
|
|
181
181
|
rake (13.4.2)
|
|
182
|
-
rbtree (0.4.
|
|
182
|
+
rbtree (0.4.7)
|
|
183
183
|
redis (5.4.1)
|
|
184
184
|
redis-client (>= 0.22.0)
|
|
185
185
|
redis-client (0.28.0)
|