Tantangan Observabilitas Pipeline CI/CD

Sebagian besar pipeline CI/CD beroperasi secara opaque. Tim platform engineering sering kesulitan melacak regresi durasi build, efisiensi alokasi runner, hingga cache miss tanpa memasang agent APM berat yang memperlambat runtime. Pendekatan intrusif seperti membungkus setiap perintah CLI dengan wrapper profiler menambahkan latency eksekusi dan meningkatkan risiko kegagalan pipeline akibat kesalahan internal tools monitoring.

Solusinya adalah CI Telemetry Harvester pasif. Pola ini memisahkan proses harvesting menjadi proses background mandiri yang membaca data runtime dari platform metadata API, file sistem, atau stream log runner, lalu mengirimkan payload metrik secara asinkron tanpa menahan proses exit pipeline utama.

Arsitektur CI Telemetry Harvester Pasif

Harvester dirancang dengan prinsip zero-impact terhadap status build:

  • Non-blocking lifecycle: Harvester berjalan di background via daemon process atau dijalankan khusus pada step pasca-eksekusi (post-job hook) menggunakan status flag always().
  • Fault tolerance: Kesalahan fatal pada koneksi jaringan, parsing metrik, atau API ingest tidak boleh menghasilkan exit code non-zero yang menggagalkan build runner.
  • Deterministic payload: Menghasilkan skema JSON yang konsisten untuk metrik eksekusi, cache status, dan alokasi memori.

Implementasi Harvester CLI dengan Go

Go dipilih karena menghasilkan single binary portabel tanpa dependensi runtime tambahan. Tool berikut membaca penggunaan memori Linux via /proc/meminfo dan status cache dari environment variable GitHub Actions, lalu mengirimkan metrik dengan batas timeout ketat.

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

type Payload struct {
	WorkflowRunID string            `json:"workflow_run_id"`
	Repository    string            `json:"repository"`
	DurationSec   float64           `json:"duration_sec"`
	MemoryUsedKB  uint64            `json:"memory_used_kb"`
	CacheHit      bool              `json:"cache_hit"`
	StepStatuses  map[string]string `json:"step_statuses"`
}

func getMemoryUsage() uint64 {
	data, err := os.ReadFile("/proc/meminfo")
	if err != nil {
		return 0
	}
	var memTotal, memAvailable uint64
	lines := strings.Split(string(data), "\n")
	for _, line := range lines {
		fields := strings.Fields(line)
		if len(fields) < 2 {
			continue
		}
		if fields[0] == "MemTotal:" {
			memTotal, _ = strconv.ParseUint(fields[1], 10, 64)
		}
		if fields[0] == "MemAvailable:" {
			memAvailable, _ = strconv.ParseUint(fields[1], 10, 64)
		}
	}
	if memTotal > memAvailable {
		return memTotal - memAvailable
	}
	return 0
}

func main() {
	startTime := time.Now()
	// Simulasi parsing waktu dari file trigger atau env
	if startStr := os.Getenv("HARVESTER_START_TIME"); startStr != "" {
		if epoch, err := strconv.ParseInt(startStr, 10, 64); err == nil {
			startTime = time.Unix(epoch, 0)
		}
	}

	payload := Payload{
		WorkflowRunID: os.Getenv("GITHUB_RUN_ID"),
		Repository:    os.Getenv("GITHUB_REPOSITORY"),
		DurationSec:   time.Since(startTime).Seconds(),
		MemoryUsedKB:  getMemoryUsage(),
		CacheHit:      os.Getenv("CACHE_HIT_STATUS") == "true",
		StepStatuses: map[string]string{
			"build": os.Getenv("STEP_BUILD_STATUS"),
			"test":  os.Getenv("STEP_TEST_STATUS"),
		},
	}

	body, err := json.Marshal(payload)
	if err != nil {
		os.Exit(0) // Silent exit untuk menghindari build failure
	}

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, os.Getenv("TELEMETRY_ENDPOINT"), bytes.NewBuffer(body))
	if err != nil {
		os.Exit(0)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-CI-Token", os.Getenv("TELEMETRY_TOKEN"))

	client := &http.Client{}
	_, _ = client.Do(req) // Error diabaikan secara sengaja
	os.Exit(0)
}

Integrasi pada GitHub Actions Workflow

Gunakan execution hook pada tahap akhir workflow. Dengan directive if: always(), step harvester tetap berjalan meski unit test atau tahapan kompilasi sebelumnya mengalami error.

name: CI Pipeline

on:
  push:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: Mark Pipeline Start
        run: echo "HARVESTER_START_TIME=$(date +%s)" >> $GITHUB_ENV

      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Restore Build Cache
        id: cache-restore
        uses: actions/cache@v4
        with:
          path: ~/.cache
          key: ${{ runner.os }}-build-${{ hashFiles('**/go.sum') }}

      - name: Build Application
        id: step_build
        run: make build

      - name: Run Tests
        id: step_test
        run: make test

      - name: Run Telemetry Harvester
        if: always()
        env:
          CACHE_HIT_STATUS: ${{ steps.cache-restore.outputs.cache-hit }}
          STEP_BUILD_STATUS: ${{ steps.step_build.outcome }}
          STEP_TEST_STATUS: ${{ steps.step_test.outcome }}
          TELEMETRY_ENDPOINT: ${{ secrets.INTERNAL_TELEMETRY_ENDPOINT }}
          TELEMETRY_TOKEN: ${{ secrets.INTERNAL_TELEMETRY_TOKEN }}
        run: |
          chmod +x ./bin/harvester
          ./bin/harvester || true

Mitigasi Risiko Kegagalan Jaringan

Salah satu kesalahan fatal dalam telemetri CI adalah pipeline berwarna merah hanya karena backend ingest down atau mengalami timeout latensi. Tiga aturan mitigasi wajib:

  1. Strict Client-Side Timeout: Terapkan batas waktu maksimal 2 detik pada HTTP client. Koneksi dialer, TLS handshake, dan payload transmission harus selesai dalam rentang tersebut atau dihentikan paksa (fail-fast).
  2. Explicit Exit Code Masking: Pastikan script shell membungkus CLI dengan pipe || true agar runner mengabaikan error exit code apa pun dari harvester.
  3. UDP atau Fire-and-Forget Ingest: Untuk skala load pengujian yang masif, pertimbangkan pengiriman paket metrik via StatsD (UDP) ke sidecar runner agar transmisi I/O sama sekali tidak terpengaruh oleh state TCP handshake.