Mikroserwis Analytics — zbieranie wyświetleń przez RabbitMQ i agregacja

Zbudowałem dedykowany serwis do zbierania i agregowania statystyk wyświetleń postów. Dane przychodzą asynchronicznie przez RabbitMQ, agregaty przechowywane w MySQL.

Po co osobny serwis?

Statystyki wyświetleń mają inne charakterystyki niż reszta systemu:

  • Zapis jest bardzo częsty (każde wyświetlenie posta)
  • Odczyt jest rzadki (dashboard admina, panel autora)
  • Dane mogą być nieaktualne do kilku sekund — to akceptowalne

To idealny kandydat na osobny serwis z innym modelem danych zoptymalizowanym pod write-heavy workload.

Schemat bazy danych

CREATE TABLE post_views (
    id          BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    post_uuid   CHAR(36) NOT NULL,
    user_id     BIGINT UNSIGNED NULL,
    ip          VARCHAR(45) NULL,
    user_agent  TEXT NULL,
    referer     VARCHAR(500) NULL,
    viewed_at   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_post_uuid (post_uuid),
    INDEX idx_viewed_at (viewed_at)
);

CREATE TABLE post_view_aggregates (
    id          BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    post_uuid   CHAR(36) NOT NULL,
    date        DATE NOT NULL,
    view_count  INT UNSIGNED NOT NULL DEFAULT 0,
    UNIQUE KEY uq_post_date (post_uuid, date)
);

post_views to surowe dane. post_view_aggregates to zagregowane widoki per dzień — szybki odczyt bez skanowania milionów rekordów.

Consumer — odbieranie eventów

class ConsumePostViews extends Command
{
    public function handle(): void
    {
        $this->channel->basic_consume(
            queue: 'analytics.post_views',
            callback: function (AMQPMessage $msg) {
                $data = json_decode($msg->body, true);

                // Zapis surowego eventu
                PostView::create([
                    'post_uuid' => $data['post_uuid'],
                    'user_id'   => $data['user_id'] ?? null,
                    'ip'        => $data['ip'],
                    'user_agent' => $data['user_agent'] ?? null,
                    'referer'   => $data['referer'] ?? null,
                    'viewed_at' => $data['timestamp'],
                ]);

                // Upsert agregatu (atomowy increment)
                DB::statement('
                    INSERT INTO post_view_aggregates (post_uuid, date, view_count)
                    VALUES (?, CURDATE(), 1)
                    ON DUPLICATE KEY UPDATE view_count = view_count + 1
                ', [$data['post_uuid']]);

                $msg->ack();
            }
        );

        while ($this->channel->is_consuming()) {
            $this->channel->wait();
        }
    }
}

API Analytics

GET /api/v1/posts/{uuid}/views          → łączna liczba wyświetleń
GET /api/v1/posts/{uuid}/views/daily    → wyświetlenia per dzień (ostatnie 30 dni)
GET /api/v1/posts/top?limit=10          → top posty wg wyświetleń

Wszystkie endpointy wymagają X-Internal-Api-Key — dostępne tylko dla innych serwisów.

Integracja z frontendem

Frontend wywołuje analytics API przy wyświetlaniu dashboardu autora. Odpytuje GET /api/v1/posts/{uuid}/views dla każdego posta i wyświetla liczbę.

Comments

No comments yet