
⏰ 🔥 بروكسي TCP لمحاكاة ظروف الشبكة والنظام لاختبار الفوضى والمرونة
Toxiproxy هو إطار عمل لمحاكاة ظروف الشبكة. صُمم خصيصًا للعمل في بيئات الاختبار والتكامل المستمر (CI) والتطوير، ويدعم العبث الحتمي بالاتصالات، مع دعم للفوضى العشوائية والتخصيص. Toxiproxy هو الأداة التي تحتاجها لإثبات بالاختبارات أن تطبيقك لا يحتوي على نقاط فشل فردية. لقد استخدمناها بنجاح في جميع بيئات التطوير والاختبار في Shopify منذ أكتوبر 2014. اطّلع على [مقالة مدونتنا][blog] حول المرونة لمزيد من المعلومات.
يتكوّن استخدام Toxiproxy من جزأين: وكيل TCP مكتوب بلغة Go (وهو ما يحتويه هذا المستودع) وعميل يتواصل مع الوكيل عبر HTTP. يمكنك ضبط تطبيقك بحيث تمر جميع اتصالات الاختبار عبر Toxiproxy ثم تتحكم في حالتها الصحية عبر HTTP. اطّلع على الاستخدام أدناه لمعرفة كيفية إعداد مشروعك.
على سبيل المثال، لإضافة 1000ms من زمن الاستجابة إلى استجابة MySQL من عميل Ruby:```ruby Toxiproxy[:mysql_master].downstream(:latency, latency: 1000).apply do Shop.first # this takes at least 1s end
لإسقاط جميع مثيلات Redis:```ruby
Toxiproxy[/redis/].down do
Shop.first # this will throw an exception
end
على الرغم من أن الأمثلة في هذا README مكتوبة حاليًا بلغة Ruby، إلا أنه لا شيء يمنعك من إنشاء عميل بأي لغة أخرى (انظر العملاء).
الأدوات الحالية التي وجدناها لم توفر نوع واجهة برمجة التطبيقات الديناميكية التي احتجناها لاختبارات
التكامل والوحدات. أدوات لينكس مثل nc وغيرها ليست
متعددة المنصات وتتطلب صلاحيات root، مما يجعلها مشكلة في بيئات
الاختبار والتطوير والتكامل المستمر.
لنستعرض مثالًا مع تطبيق Rails. لاحظ أن Toxiproxy ليس مرتبطًا بـ Ruby بأي شكل، فهو فقط كان حالة الاستخدام الأولى لدينا. يمكنك رؤية المثال الكامل على sirupsen/toxiproxy-rails-example. لتبدأ فورًا، انتقل إلى الاستخدام.
في مدونتنا الشهيرة، ولسبب ما نُخزّن الوسوم لمشاركاتنا في
Redis والمنشورات نفسها في MySQL. قد يكون لدينا كلاس Post
يتضمّن بعض الطرق للتعامل مع الوسوم في Redis set:```ruby
class Post < ActiveRecord::Base
def tags TagRedis.smembers(tag_key) end
def add_tag(tag) TagRedis.sadd(tag_key, tag) end
def remove_tag(tag) TagRedis.srem(tag_key, tag) end
def tag_key "post:tags:#{self.id}" end end
لقد قررنا أن الخطأ أثناء الكتابة إلى مخزن بيانات الوسوم
(إضافة/إزالة) مقبول. ومع ذلك، إذا كان مخزن بيانات الوسوم معطلاً، فيجب أن نتمكن
من رؤية المنشور بدون وسوم. يمكننا ببساطة التقاط
`Redis::CannotConnectError` حول استدعاء Redis الخاص بـ `SMEMBERS` في دالة
`tags`. دعنا نستخدم Toxiproxy لاختبار ذلك.
بما أننا قمنا بالفعل بتثبيت Toxiproxy وهو يعمل على جهازنا، يمكننا
الانتقال إلى الخطوة 2. هنا نحتاج إلى التأكد من أن Toxiproxy لديه تعيين لوسوم
Redis. إلى `config/boot.rb` (قبل إجراء أي اتصال) نضيف:```ruby
require 'toxiproxy'
Toxiproxy.populate([
{
name: "toxiproxy_test_redis_tags",
listen: "127.0.0.1:22222",
upstream: "127.0.0.1:6379"
}
])
ثم في config/environments/test.rb نضبط TagRedis ليكون عميل Redis
يتصل بـ Redis عبر Toxiproxy بإضافة هذا السطر:```ruby
TagRedis = Redis.new(port: 22222)
جميع الاستدعاءات في بيئة الاختبار تمر الآن عبر Toxiproxy. وهذا يعني أنه يمكننا
إضافة اختبار وحدة حيث نحاكي فشلاً:```ruby
test "should return empty array when tag redis is down when listing tags" do
@post.add_tag "mammals"
# Take down all Redises in Toxiproxy
Toxiproxy[/redis/].down do
assert_equal [], @post.tags
end
end
يَفشل الاختبار مع Redis::CannotConnectError. رائع! قام Toxiproxy بإيقاف تشغيل خادم Redis بنجاح طوال فترة الإغلاق. دعنا نُصلح دالة tags لتكون أكثر مرونة:```ruby
def tags
TagRedis.smembers(tag_key)
rescue Redis::CannotConnectError
[]
end
الاختبارات تنجح! لدينا الآن اختبار وحدة يثبت أن جلب الوسوم عند تعطل Redis يُرجع مصفوفة فارغة، بدلاً من رمي استثناء. لتغطية شاملة، يجب أيضاً كتابة اختبار تكامل يغطي جلب صفحة المقال كاملة عند تعطل Redis.
التطبيق المثالي الكامل موجود في [sirupsen/toxiproxy-rails-example](https://github.com/sirupsen/toxiproxy-rails-example).
## الاستخدام
يتكوّن إعداد مشروع لاستخدام Toxiproxy من ثلاث خطوات:
1. تثبيت Toxiproxy
2. ملء Toxiproxy
3. استخدام Toxiproxy
### 1. تثبيت Toxiproxy
**لينكس**
انظر [`Releases`](https://github.com/Shopify/toxiproxy/releases) للحصول على أحدث الملفات الثنائية والحزم النظامية لمعمارية جهازك.
**أوبونتو**```bash
$ wget -O toxiproxy-2.1.4.deb https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy_2.1.4_amd64.deb
$ sudo dpkg -i toxiproxy-2.1.4.deb
$ sudo service toxiproxy start
OS X
باستخدام Homebrew:```bash $ brew tap shopify/shopify $ brew install toxiproxy
أو مع [MacPorts](https://www.macports.org/):```bash
$ port install toxiproxy
ويندوز
يتوفر Toxiproxy لنظام ويندوز للتحميل على https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy-server-windows-amd64.exe
دوكر
يتوفر Toxiproxy على سجل حاويات GitHub.
الإصدارات القديمة <= 2.1.4 متوفرة على Docker Hub.```bash
$ docker pull ghcr.io/shopify/toxiproxy
$ docker run --rm -it ghcr.io/shopify/toxiproxy
إذا كنت تستخدم Toxiproxy من المضيف بدلاً من الحاويات الأخرى، فعّل شبكة المضيف باستخدام `--net=host`.```shell
$ docker run --rm --entrypoint="/toxiproxy-cli" -it ghcr.io/shopify/toxiproxy list
إذا كان لديك Go مثبتًا، يمكنك بناء Toxiproxy من المصدر باستخدام ملف make:```bash $ make build $ ./toxiproxy-server
#### الترقية من Toxiproxy 1.x
في Toxiproxy 2.0 تم إجراء عدة تغييرات على واجهة البرمجة (API) تجعلها غير متوافقة مع الإصدار 1.x.
من أجل استخدام الإصدار 2.x من خادم Toxiproxy، ستحتاج إلى التأكد من أن مكتبة
العميل لديك تدعم نفس الإصدار. يمكنك التحقق من إصدار Toxiproxy الذي تعمل به من خلال
النظر إلى نقطة النهاية `/version`.
راجع الوثائق الخاصة بمكتبة العميل لديك لمعرفة تغييرات المكتبة المحددة. التغييرات التفصيلية
لخادم Toxiproxy يمكن العثور عليها في [CHANGELOG.md](https://github.com/shopify/toxiproxy/blob/HEAD/CHANGELOG.md).
### 2. ملء Toxiproxy
عند تشغيل تطبيقك، يجب عليه التأكد من أن Toxiproxy يعرف
أي نقاط النهاية يجب توجيهها وإلى أين. المعاملات الرئيسية هي: الاسم، وعنوان Toxiproxy
للاستماع عليه، وعنوان الخادم العلوي (upstream).
بعض مكتبات العملاء توفر أدوات مساعدة لهذه المهمة، والتي هي في الأساس مجرد
التأكد من إنشاء كل وكيل (proxy) في قائمة. مثال من عميل Ruby:```ruby
# Make sure `shopify_test_redis_master` and `shopify_test_mysql_master` are
# present in Toxiproxy
Toxiproxy.populate([
{
name: "shopify_test_redis_master",
listen: "127.0.0.1:22220",
upstream: "127.0.0.1:6379"
},
{
name: "shopify_test_mysql_master",
listen: "127.0.0.1:24220",
upstream: "127.0.0.1:3306"
}
])
يجب تشغيل هذا الكود في أقرب وقت ممكن أثناء الإقلاع (boot)، قبل أن يُنشئ أي كود اتصالًا عبر Toxiproxy. يُرجى مراجعة مكتبة العميل لديك للاطلاع على توثيق أدوات المساعدة الخاصة بالتعبئة (population helpers).
بدلاً من ذلك، استخدم CLI لإنشاء الوكلاء (proxies)، على سبيل المثال:```bash toxiproxy-cli create -l localhost:26379 -u localhost:6379 shopify_test_redis_master
نوصي باستخدام تسمية مثل ما سبق: `<app>_<env>_<data store>_<shard>`.
يضمن هذا عدم وجود تعارض بين التطبيقات التي تستخدم نفس
Toxiproxy.
بالنسبة للتطبيقات الكبيرة، نوصي بتخزين إعدادات Toxiproxy في
ملف إعدادات منفصل. نستخدم `config/toxiproxy.json`. يمكن تمرير هذا الملف إلى
الخادم باستخدام الخيار `-config`، أو تحميله بواسطة التطبيق
لاستخدامه مع دالة `populate`.
مثال على `config/toxiproxy.json`:```json
[
{
"name": "web_dev_frontend_1",
"listen": "[::]:https://raw.githubusercontent.com/shopify/toxiproxy/HEAD/18080%22,
"upstream": "webapp.domain:8080",
"enabled": true
},
{
"name": "web_dev_mysql_1",
"listen": "[::]:13306",
"upstream": "database.domain:3306",
"enabled": true
}
]
استخدم منافذ خارج نطاق المنافذ المؤقتة لتجنب تعارضات المنافذ العشوائية.
النطاق الافتراضي هو من 32,768 إلى 61,000 على Linux، انظر
/proc/sys/net/ipv4/ip_local_port_range.
لاستخدام Toxiproxy، تحتاج الآن إلى تكوين تطبيقك للاتصال عبر Toxiproxy. بمتابعة مثالنا من الخطوة الثانية، يمكننا تكوين عميل Redis للاتصال عبر Toxiproxy:```ruby
redis = Redis.new(port: 6380)
redis = Redis.new(port: 22220)
الآن يمكنك التلاعب بها عبر واجهة برمجة تطبيقات Toxiproxy. في Ruby:```ruby
redis = Redis.new(port: 22220)
Toxiproxy[:shopify_test_redis_master].downstream(:latency, latency: 1000).apply do
redis.get("test") # will take 1s
end
أو عبر CLI:```bash toxiproxy-cli toxic add -t latency -a latency=1000 shopify_test_redis_master
Please consult your respective client library on usage.
### 4. التسجيل
توجد مستويات السجل التالية: panic، fatal، error، warn أو warning، info، debug وtrace.
يمكن تحديث المستوى عبر متغير البيئة `LOG_LEVEL`.
### السموم
تقوم السموم بالتحكم في الأنبوب بين العميل والمنبع. يمكن إضافتها وإزالتها من الوكلاء باستخدام [HTTP api](#http-api). لكل سُم معلماته الخاصة لتغيير كيفية تأثيره على روابط الوكيل.
للاطلاع على توثيق تنفيذ السموم المخصصة، راجع [CREATING_TOXICS.md](https://github.com/shopify/toxiproxy/blob/HEAD/CREATING_TOXICS.md)
#### latency
أضف تأخيرًا إلى جميع البيانات التي تمر عبر الوكيل. التأخير يساوي `latency` +/- `jitter`.
السمات:
- `latency`: الوقت بالمللي ثانية
- `jitter`: الوقت بالمللي ثانية
#### down
إيقاف خدمة ليس سُمًا من الناحية التقنية في تنفيذ Toxiproxy. يتم ذلك عن طريق إرسال طلب `POST` إلى `/proxies/{proxy}` وتعيين حقل `enabled` إلى `false`.
#### bandwidth
قم بتحديد اتصال إلى عدد أقصى من الكيلوبايتات في الثانية.
السمات:
- `rate`: المعدل بالكيلوبايت في الثانية (KB/s)
#### slow_close
قم بتأخير إغلاق مقبس TCP حتى انقضاء `delay`.
السمات:
- `delay`: الوقت بالمللي ثانية
#### timeout
يوقف مرور جميع البيانات، ويغلق الاتصال بعد `timeout`. إذا كانت قيمة `timeout` هي 0، فلن يُغلق الاتصال، وستُسقَط البيانات حتى تتم إزالة السُم.
السمات:
- `timeout`: الوقت بالمللي ثانية
#### reset_peer
محاكاة TCP RESET (إعادة تعيين الاتصال بواسطة النظير) على الاتصالات عن طريق إغلاق الإدخال الوهمي (stub Input) فورًا أو بعد `timeout`.
السمات:
- `timeout`: الوقت بالمللي ثانية
#### slicer
يقطع بيانات TCP إلى أجزاء صغيرة، مع إمكانية إضافة تأخير بين كل "حزمة" مقطوعة.
السمات:
- `average_size`: الحجم بالبايت لمتوسط الحزمة
- `size_variation`: التباين بالبايت لمتوسط الحزمة (يجب أن يكون أصغر من average_size)
- `delay`: الوقت بالميكروثانية لتأخير كل حزمة
#### limit_data
يغلق الاتصال عندما تتجاوز البيانات المنقولة الحد.
- `bytes`: عدد البايتات التي يجب نقلها قبل إغلاق الاتصال
#### packet_loss
يُسقط عشوائيًا أجزاءً (chunks) تمر عبر الوكيل لمحاكاة ظروف شبكة Wi-Fi أو شبكات الجوال أو الأقمار الصناعية غير المستقرة.
السمات:
- `loss_rate`: احتمال [0.0-1.0] إسقاط جزء (الافتراضي 0.0)
- `correlation`: احتمال إسقاط إضافي عند إسقاط الجزء السابق، لمحاكاة فقدان الاندفاع (الافتراضي 0.0)
### HTTP API
جميع الاتصالات بين العميل وخدمة Toxiproxy (daemon) تتم من خلال واجهة HTTP، وهي موصوفة هنا.
يستمع Toxiproxy لاتصالات HTTP على المنفذ **8474**.
#### حقول الوكيل:
- `name`: اسم الوكيل (سلسلة نصية)
- `listen`: عنوان الاستماع (سلسلة نصية)
- `upstream`: عنوان المنبع للوكيل (سلسلة نصية)
- `enabled`: true/false (الافتراضي هو true عند الإنشاء)
لتغيير اسم الوكيل، يجب حذفه وإعادة إنشائه.
تغيير حقلي `listen` أو `upstream` سيؤدي إلى إعادة تشغيل الوكيل وإسقاط أي اتصالات نشطة.
إذا تم تحديد `listen` بمنفذ 0، فسيختار toxiproxy منفذًا مؤقتًا. سيتم تحديث حقل `listen` في الاستجابة بالمنفذ الفعلي.
إذا غيّرت `enabled` إلى `false`، فسيؤدي ذلك إلى إيقاف تشغيل الوكيل. يمكنك إعادته إلى `true` لإعادة تمكينه.
#### حقول السُم:
- `name`: اسم السُم (سلسلة نصية، الافتراضي هو `<type>_<stream>`)
- `type`: نوع السُم (سلسلة نصية)
- `stream`: اتجاه الرابط المطلوب التأثير عليه (الافتراضي هو `downstream`)
- `toxicity`: احتمال تطبيق السُم على رابط (الافتراضي 1.0، أي 100%)
- `attributes`: خريطة من السمات الخاصة بالسُم
انظر [Toxics](#toxics) للحصول على السمات الخاصة بالسُم.
يجب أن يكون اتجاه `stream` إما `upstream` أو `downstream`. يطبق `upstream` السُم على اتصال `client -> server`، بينما يطبق `downstream` السُم على اتصال `server -> client`. يمكن استخدام هذا لتعديل الطلبات والاستجابات بشكل منفصل.
#### نقاط النهاية
جميع نقاط النهاية بتنسيق JSON.
- **GET /proxies** - قائمة بالوكلاء الموجودين وسمومهم
- **POST /proxies** - إنشاء وكيل جديد
- **POST /populate** - إنشاء أو استبدال قائمة الوكلاء
- **GET /proxies/{proxy}** - عرض الوكيل مع جميع سمومه النشطة
- **POST /proxies/{proxy}** - تحديث حقول الوكيل
- **DELETE /proxies/{proxy}** - حذف وكيل موجود
- **GET /proxies/{proxy}/toxics** - قائمة السموم النشطة
- **POST /proxies/{proxy}/toxics** - إنشاء سُم جديد
- **GET /proxies/{proxy}/toxics/{toxic}** - الحصول على حقول سُم نشط
- **POST /proxies/{proxy}/toxics/{toxic}** - تحديث سُم نشط
- **DELETE /proxies/{proxy}/toxics/{toxic}** - إزالة سُم نشط
- **POST /reset** - تمكين جميع الوكلاء وإزالة جميع السموم النشطة
- **GET /version** - يعيد رقم إصدار الخادم
- **GET /metrics** - يعيد مقاييس متوافقة مع Prometheus
#### تعبئة الوكلاء
يمكن إضافة الوكلاء وتكوينهم بشكل مجمّع باستخدام نقطة النهاية `/populate`. يتم ذلك عن طريق تمرير مصفوفة JSON من الوكلاء إلى toxiproxy. إذا كان هناك وكيل بنفس الاسم موجودًا بالفعل، فسيتم مقارنته بالوكيل الجديد واستبداله إذا لم يتطابق عنوان `upstream` وعنوان `listen`.
يمكن تضمين استدعاء `/populate` على سبيل المثال عند بدء تشغيل التطبيق لضمان وجود جميع الوكلاء المطلوبين. من الآمن إجراء هذا الاستدعاء عدة مرات، حيث لن يتم تعديل الوكلاء طالما كانت حقولهم متوافقة مع البيانات الجديدة.
### مثال CLI```bash
$ toxiproxy-cli create -l localhost:26379 -u localhost:6379 redis
Created new proxy redis
$ toxiproxy-cli list
Listen Upstream Name Enabled Toxics
======================================================================
127.0.0.1:26379 localhost:6379 redis true None
Hint: inspect toxics with `toxiproxy-client inspect <proxyName>`
The input chunk is empty — no source text was provided to translate. Please supply the content for chunk 41.```bash $ redis-cli -p 26379 127.0.0.1:26379> SET omg pandas OK 127.0.0.1:26379> GET omg "pandas"
لا يوجد محتوى مصدري في الإدخال لترجمته.```bash
$ toxiproxy-cli toxic add -t latency -a latency=1000 redis
Added downstream latency toxic 'latency_downstream' on proxy 'redis'
عند التعامل مع أنظمة كبيرة ومعقدة، من الضروري فهم كيفية تفاعل المكونات المختلفة. يوفّر هذا القسم نظرة عامة على البنية العامة والتدفق بين الوحدات الرئيسية.
[الإدخال] -> [المحلّل] -> [المعالج] -> [المخرجات]
يوضح المخطط أعلاه التدفق الأساسي للبيانات عبر النظام. يبدأ كل شيء بوحدة الإدخال، التي تقرأ البيانات الخام من مصادر مختلفة. بعد ذلك، يقوم المحلّل بتحويل هذه البيانات إلى تمثيل داخلي يمكن للمعالج التعامل معه. أخيرًا، تقوم وحدة المخرجات بتسلسل النتائج وإرسالها إلى الوجهة المطلوبة.
ملاحظة: يمكن توسيع كل مكوّن عبر واجهات برمجية (APIs) موثقة، مما يسمح بوصلات مخصصة دون تعديل النواة الأساسية.
لمزيد من التفاصيل حول خيارات التهيئة المتقدمة، راجع ملف الإعدادات النموذجي المرفق مع التوزيعة.```bash $ redis-cli -p 26379 127.0.0.1:26379> GET omg "pandas" (1.00s) 127.0.0.1:26379> DEL omg (integer) 1 (1.00s)
I notice the input section is empty—there's no actual content provided to translate. Please provide the chunk content (the Markdown text from the Kitploit tool README) so I can translate it into Arabic according to your rules.```bash
$ toxiproxy-cli toxic remove -n latency_downstream redis
Removed toxic 'latency_downstream' on proxy 'redis'
Please provide the Markdown content to translate.```bash $ redis-cli -p 26379 127.0.0.1:26379> GET omg (nil)
لم يتم تقديم أي محتوى للترجمة في الحقل "INPUT". الرجاء إرسال نص القطعة المطلوب ترجمته.```bash
$ toxiproxy-cli delete redis
Deleted proxy redis
(empty)```bash $ redis-cli -p 26379 Could not connect to Redis at 127.0.0.1:26379: Connection refused
### المقاييس
يعرض Toxiproxy مقاييس متوافقة مع Prometheus عبر واجهة HTTP API الخاصة به على /metrics.
انظر [METRICS.md](https://github.com/shopify/toxiproxy/blob/HEAD/METRICS.md) للحصول على الأوصاف الكاملة
### الأسئلة الشائعة
**ما مدى سرعة Toxiproxy؟** تعتمد سرعة Toxiproxy إلى حد كبير على عتادك،
ولكن يمكنك توقع زمن استجابة يبلغ *< 100µs* عندما لا تكون أي toxics مفعّلة. عند التشغيل
باستخدام `GOMAXPROCS=4` على جهاز Macbook Pro حققنا إنتاجية تبلغ *~1000MB/s*، وقد تصل
إلى *2400MB/s* على أجهزة سطح مكتب أعلى مواصفة. باختصار، يمكنك توقع أن ينقل Toxiproxy
البيانات بسرعة لا تقل عن سرعة التطبيق الذي تختبره.
**هل يمكن لـ Toxiproxy إجراء اختبارات عشوائية؟** يمكن تكوين العديد من toxics المتاحة
بحيث تشمل العشوائية، مثل `jitter` في toxic `latency`. هناك أيضًا
معامل عام `toxicity` يحدد النسبة المئوية للاتصالات التي سيؤثر عليها
toxic. وهذا مفيد بشكل خاص لأشياء مثل toxic `timeout`، والذي
يسمح لـ X% من الاتصالات بأن تنتهي مهلتها.
**لا أرى إجراءات Toxiproxy تنعكس على MySQL**. يفضّل MySQL
استخدام مقبس Unix domain socket المحلي لبعض العملاء، بغض النظر عن المنفذ الذي تمرره
إذا كان المضيف مضبوطًا على `localhost`. قم بتكوين خادم MySQL بحيث لا ينشئ
مقبسًا، واستخدم `127.0.0.1` كمضيف. تذكر إزالة المقبس القديم
بعد إعادة تشغيل الخادم.
**يسبب Toxiproxy فشلًا متقطعًا في الاتصال**. استخدم منافذ خارج
نطاق المنافذ المؤقتة (ephemeral ports) لتجنب تعارضات المنافذ العشوائية. وهو من `32,768` إلى `61,000` على
Linux افتراضيًا، انظر `/proc/sys/net/ipv4/ip_local_port_range`.
**هل يجب تشغيل Toxiproxy لكل تطبيق؟** لا، نوصي باستخدام
نفس Toxiproxy لجميع التطبيقات. للتمييز بين الخدمات،
نوصي بتسمية البروكسيات الخاصة بك وفق المخطط: `<app>_<env>_<data store>_<shard>`.
على سبيل المثال، `shopify_test_redis_master` أو `shopify_development_mysql_1`.
### التطوير
* `make`. أنشئ ملفًا ثنائيًا (binary) من toxiproxy للتطوير على المنصة الحالية.
* `make all`. أنشئ الملفات الثنائية والحزم الخاصة بـ Toxiproxy لجميع المنصات. يتطلب
أن يكون Go مُجمَّعًا مع تفعيل التجميع المتقاطع (cross compilation) على Linux وDarwin (amd64)
بالإضافة إلى وجود [`goreleaser`](https://goreleaser.com/) في `$PATH` الخاص بك
لبناء الملفات الثنائية وحزمة Linux.
* `make test`. شغّل اختبارات Toxiproxy.
### الإصدار
انظر [RELEASE.md](https://github.com/shopify/toxiproxy/blob/HEAD/RELEASE.md)
[blog]: https://shopify.engineering/building-and-testing-resilient-ruby-on-rails-applications