Let's Go Further معیارها › ایجاد معیارهای سفارشی
قبلی · فهرست مطالب · بعدی
فصل ۱۸.۲.

ایجاد معیارهای سفارشی

اطلاعات پیش‌فرضی که توسط handler expvar ارائه می‌شود، نقطه شروع خوبی است، اما می‌توانیم با نمایش برخی معیارهای سفارشی اضافی در پاسخ JSON، آن را مفیدتر کنیم.

برای نشان دادن این موضوع، با یک مثال ساده شروع می‌کنیم و ابتدا نسخه برنامه خود را در JSON نمایش می‌دهیم. اگر به یاد ندارید، شماره نسخه در حال حاضر به عنوان یک ثابت رشته‌ای "1.0.0" در فایل main.go ما تعریف شده است.

کد انجام این کار به دو مرحله اصلی تقسیم می‌شود: ابتدا باید یک متغیر سفارشی با پکیج expvar ثبت کنیم، و سپس باید مقدار خود متغیر را تنظیم کنیم. در یک خط، کد تقریباً به این صورت است:

expvar.NewString("version").Set(version)

بخش اول این کد — expvar.NewString("version") — یک نوع expvar.String جدید ایجاد می‌کند، سپس آن را منتشر می‌کند تا در پاسخ JSON handler expvar با نام "version" ظاهر شود، و سپس یک اشاره‌گر به آن برمی‌گرداند. سپس از متد Set() روی آن استفاده می‌کنیم تا یک مقدار واقعی به اشاره‌گر اختصاص دهیم.

دو نکته دیگر:

بیایید این کد را در تابع main() خود ادغام کنیم، به این صورت:

فایل: cmd/api/main.go
package main

import (
    "context"
    "database/sql"
    "expvar" // New import
    "flag"
    "log/slog"
    "os"
    "strings"
    "sync"
    "time"

    "greenlight.alexedwards.net/internal/data"
    "greenlight.alexedwards.net/internal/mailer"

    _ "github.com/lib/pq"
)

// Remember, our version number is just a constant string (for now).
const version = "1.0.0"

...

func main() {
    ...

    // Publish a new "version" variable in the expvar handler containing our application
    // version number (currently the constant "1.0.0").
    expvar.NewString("version").Set(version)

    app := &application{
        config: cfg,
        logger: logger,
        models: data.NewModels(db),
        mailer: mailer,
    }

    err = app.serve()
    if err != nil {
        logger.Error(err.Error())
        os.Exit(1)
    }
}

...

اگر API را مجدداً راه‌اندازی کنید و http://localhost:4000/debug/vars را در مرورگر خود باز کنید، اکنون باید یک آیتم "version": "1.0.0" در JSON ببینید.

مشابه این:

18.02-01.png

معیارهای پویا

گاهی اوقات ممکن است بخواهید معیارهایی را منتشر کنید که نیاز به فراخوانی کد دیگر — یا انجام نوعی پیش‌پردازش — برای تولید اطلاعات لازم دارند. برای کمک به این کار، تابع expvar.Publish() وجود دارد که به شما امکان می‌دهد نتیجه یک تابع را در خروجی JSON منتشر کنید.

به عنوان مثال، اگر بخواهید تعداد goroutineهای فعال فعلی را از تابع runtime.NumGoroutine() Go منتشر کنید، می‌توانید کد زیر را بنویسید:

expvar.Publish("goroutines", expvar.Func(func() any {
    return runtime.NumGoroutine()
}))

نکته مهمی که باید به آن اشاره کنیم این است که مقدار any که از این تابع برمی‌گردد باید بدون هیچ خطایی به JSON کدگذاری شود. اگر نتواند به JSON کدگذاری شود، از خروجی expvar حذف شده و پاسخ از endpoint GET /debug/vars ناقص خواهد بود. هر خطایی به صورت خاموش نادیده گرفته می‌شود.

در مورد کد نمونه بالا، runtime.NumGoroutine() یک نوع int معمولی برمی‌گرداند — که به یک عدد JSON کدگذاری می‌شود. بنابراین مشکلی در اینجا وجود ندارد.

خب، بیایید این کد را به تابع main() خود اضافه کنیم، همراه با دو تابع دیگر که:

فایل: cmd/api/main.go
package main

import (
    "context"
    "database/sql"
    "expvar"
    "flag"
    "log/slog"
    "os"
    "runtime" // New import
    "strings"
    "sync"
    "time"

    "greenlight.alexedwards.net/internal/data"
    "greenlight.alexedwards.net/internal/mailer"

    _ "github.com/lib/pq"
)

...

func main() {
    ...

    expvar.NewString("version").Set(version)

    // Publish the number of active goroutines.
    expvar.Publish("goroutines", expvar.Func(func() any {
        return runtime.NumGoroutine()
    }))

    // Publish the database connection pool statistics.
    expvar.Publish("database", expvar.Func(func() any {
        return db.Stats()
    }))

    // Publish the current Unix timestamp.
    expvar.Publish("timestamp", expvar.Func(func() any {
        return time.Now().Unix()
    }))

    app := &application{
        config: cfg,
        logger: logger,
        models: data.NewModels(db),
        mailer: mailer,
    }

    err = app.serve()
    if err != nil {
        logger.Error(err.Error())
        os.Exit(1)
    }
}

...

اگر API را مجدداً راه‌اندازی کنید و endpoint GET /debug/vars را دوباره در مرورگر خود باز کنید، اکنون باید آیتم‌های اضافی "database"، "goroutines" و "timestamp" را در JSON ببینید. به این صورت:

18.02-02.png

در مورد من، می‌توانم ببینم که برنامه در حال حاضر 8 goroutine فعال دارد و pool اتصال پایگاه داده در وضعیت ‘اولیه’ خود با فقط یک اتصال idle قرار دارد (که هنگام فراخوانی db.PingContext() توسط کد ما در هنگام راه‌اندازی ایجاد شده است).

اگر مایل باشید، می‌توانید از ابزاری مانند hey برای تولید تعدادی درخواست به برنامه خود استفاده کنید و ببینید این اعداد تحت بار چگونه تغییر می‌کنند. به عنوان مثال، می‌توانید یک دسته درخواست به endpoint POST /v1/tokens/authentication (که کند و پرهزینه است زیرا یک رمز عبور bcrypt-hashed را بررسی می‌کند) به این صورت ارسال کنید:

$ BODY='{"email": "alice@example.com", "password": "pa55word"}'
$ hey -d "$BODY" -m "POST" http://localhost:4000/v1/tokens/authentication

Summary:
  Total:        8.0979 secs
  Slowest:      2.4612 secs
  Fastest:      1.6169 secs
  Average:      1.9936 secs
  Requests/sec: 24.6977
  
  Total data:   24975 bytes
  Size/request: 124 bytes

Response time histogram:
  1.617 [1]  |■
  1.701 [6]  |■■■■■
  1.786 [10] |■■■■■■■■■
  1.870 [26] |■■■■■■■■■■■■■■■■■■■■■■■
  1.955 [36] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  2.039 [46] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  2.123 [36] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  2.208 [21] |■■■■■■■■■■■■■■■■■■
  2.292 [12] |■■■■■■■■■■
  2.377 [4]  |■■■
  2.461 [2]  |■■


Latency distribution:
  10% in 1.8143 secs
  25% in 1.8871 secs
  50% in 1.9867 secs
  75% in 2.1000 secs
  90% in 2.2017 secs
  95% in 2.2642 secs
  99% in 2.3799 secs

Details (average, fastest, slowest):
  DNS+dialup:	0.0009 secs, 1.6169 secs, 2.4612 secs
  DNS-lookup:	0.0005 secs, 0.0000 secs, 0.0030 secs
  req write:	0.0002 secs, 0.0000 secs, 0.0051 secs
  resp wait:	1.9924 secs, 1.6168 secs, 2.4583 secs
  resp read:	0.0000 secs, 0.0000 secs, 0.0001 secs

Status code distribution:
  [201]	200 responses

اگر در حین اجرای ابزار hey از endpoint GET /debug/vars بازدید کنید، باید ببینید که معیارهای برنامه شما اکنون بسیار متفاوت به نظر می‌رسند:

18.02-03.png

در لحظه‌ای که این اسکرین‌شات را گرفتم، می‌توانیم ببینیم که برنامه API من 118 goroutine فعال، 11 اتصال پایگاه داده در حال استفاده و 14 اتصال idle داشته است.

چند نکته جالب دیگر هم وجود دارد.

عدد WaitCount پایگاه داده که 25 است، تعداد کل دفعاتی است که برنامه ما مجبور شده برای در دسترس قرار گرفتن یک اتصال پایگاه داده در pool sql.DB ما صبر کند (زیرا تمام اتصالات در حال استفاده بودند). به همین ترتیب، WaitCountDuration مقدار تجمعی زمان (بر حسب نانوثانیه) صرف شده برای انتظار برای یک اتصال است. از اینها، می‌توان محاسبه کرد که هنگامی که برنامه ما مجبور به انتظار برای یک اتصال پایگاه داده شد، زمان انتظار متوسط تقریباً 98 میلی‌ثانیه بود. در حالت ایده‌آل، در شرایط عادی بار در محیط production، باید صفر یا اعداد بسیار کمی برای این دو مورد ببینید.

همچنین، عدد MaxIdleTimeClosed تعداد کل اتصالاتی است که به دلیل رسیدن به حد ConnMaxIdleTime خود (که در مورد ما به طور پیش‌فرض روی 15 دقیقه تنظیم شده است) بسته شده‌اند. اگر برنامه را در حال اجرا رها کنید اما از آن استفاده نکنید و پس از 15 دقیقه برگردید، باید ببینید که تعداد اتصالات باز به صفر کاهش یافته و شمارنده MaxIdleTimeClosed به همین ترتیب افزایش یافته است.

شاید دوست داشته باشید با این موضوع بازی کنید و برخی از پارامترهای پیکربندی pool اتصال را تغییر دهید تا ببینید چگونه بر رفتار این اعداد تحت بار تأثیر می‌گذارد. به عنوان مثال:

$ go run ./cmd/api -limiter-enabled=false -db-max-open-conns=50 -db-max-idle-conns=50 -db-max-idle-time=20s -port=4000 

اطلاعات اضافی

محافظت از endpoint معیارها

آگاه بودن از این موضوع مهم است که این معیارها اطلاعات بسیار مفیدی به هر کسی که می‌خواهد حمله denial-of-service علیه برنامه شما انجام دهد، ارائه می‌دهند، و مقادیر "cmdline" ممکن است اطلاعات حساس بالقوه (مانند DSN پایگاه داده) را نیز فاش کنند.

بنابراین باید مطمئن شوید که دسترسی به endpoint GET /debug/vars را در محیط production محدود می‌کنید.

رویکردهای مختلفی وجود دارد که می‌توانید برای انجام این کار استفاده کنید.

یک گزینه این است که از فرآیند احراز هویت موجود خود استفاده کنیم و یک مجوز metrics:view ایجاد کنیم تا فقط کاربران مورد اعتماد خاصی بتوانند به endpoint دسترسی داشته باشند. گزینه دیگر استفاده از HTTP Basic Authentication برای محدود کردن دسترسی به endpoint است.

در مورد ما، وقتی برنامه خود را در محیط production مستقر می‌کنیم، آن را به عنوان یک reverse proxy پشت Caddy اجرا خواهیم کرد. به عنوان بخشی از تنظیمات Caddy خود، دسترسی به endpoint GET /debug/vars را محدود خواهیم کرد تا فقط از طریق اتصالات از ماشین محلی قابل دسترسی باشد، به جای اینکه در اینترنت در معرض دید قرار گیرد.

حذف معیارهای پیش‌فرض

در حال حاضر امکان حذف آیتم‌های پیش‌فرض "cmdline" و "memstats" از handler expvar وجود ندارد، حتی اگر بخواهید. یک issue باز در این مورد وجود دارد و امیدواریم در نسخه آینده Go امکان حذف این موارد فراهم شود.