Skip to content

Instantly share code, notes, and snippets.

@r3code
Created March 11, 2026 11:00
Show Gist options
  • Select an option

  • Save r3code/ba698659eff593e98444412acb09f65f to your computer and use it in GitHub Desktop.

Select an option

Save r3code/ba698659eff593e98444412acb09f65f to your computer and use it in GitHub Desktop.
PHP код для отправки метрик в Prometheus Pushgateway

PHP-код для отправки метрик в Prometheus Pushgateway

Установить зависисмости:

composer require promphp/prometheus_client_php promphp/prometheus_push_gateway_php

Код обертки для подготовки метрик PHP-приложения работающего в Kubernetes в Pushgateway/PushgatewayClient.php

Пример подключения к приложению в bootstrap/metrics.php

Пример подсчета метрик в count_mentrics_example.php

Это заготовка кода - адаптируйте под себя.

Что делает

Перед отправкой считывает параметры оружения Kubernetes и добавляет метки.
OpenTelemetry метки:

  • k8s_cluster_name
  • k8s_namespace_name
  • k8s_pod_name

Legacy-метки (для обратной совместимости):

  • k8s_cluster
  • namespace
  • pod
<?php
// Бизнес-логика приложения
// ...
// Инициализация
$registry = require __DIR__ . '/../bootstrap/metrics.php';
$counter = $registry->getOrRegisterCounter(
namespace: 'app',
name: 'orders_processed_total',
help: 'Total processed orders',
labels: ['status', 'payment_method']
);
// Подсчет метрики
$counter->inc(['status' => 'success', 'payment_method' => 'card']);
// → 6 K8s-лейблов добавятся автоматически "под капотом"
<?php
// file::bootstrap/metrics.php
require_once __DIR__ . '/../vendor/autoload.php';
use App\Pushgateway\PushgatewayClient;
$pusher = PushgatewayClient::create(
gatewayUrl: getenv('PUSHGATEWAY_URL') ?: 'http://pushgateway:9091',
serviceName: 'my-php-service',
options: [
'push_interval_seconds' => 30,
'http_timeout_seconds' => 2,
'skip_ssl_verify' => getenv('APP_ENV') === 'dev', // только в dev!
'auth_token' => getenv('PUSHGATEWAY_AUTH_TOKEN'), // опционально
'extra_labels' => [
'app_version' => getenv('APP_VERSION') ?: 'unknown',
'environment' => getenv('APP_ENV') ?: 'production',
],
]
);
// Запуск для CLI / worker-процессов
if (PHP_SAPI === 'cli') {
$pusher->start();
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, fn() => $pusher->stop());
pcntl_signal(SIGINT, fn() => $pusher->stop());
}
}
// Для FPM: отправляем при завершении запроса
// if (PHP_SAPI === 'fpm-fcgi') {
// register_shutdown_function(fn() => $pusher->pushOnce());
// }
return $pusher->getRegistry();
<?php
// file::Pushgateway/PushgatewayClient.php
declare(strict_types=1);
namespace App\Pushgateway;
use Prometheus\CollectorRegistry;
use Prometheus\Storage\Adapter;
use Prometheus\Storage\APC;
use PrometheusPushGateway\PushGateway as BasePushGateway;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\HandlerStack;
use RuntimeException;
/**
* Клиент для отправки метрик в Prometheus Pushgateway из PHP-приложений в Kubernetes.
*
* Использует официальную библиотеку promphp/prometheus_push_gateway_php как транспорт,
* добавляя специфичную для нашей инфраструктуры логику:
* - автоматическое извлечение K8s-метаданных с каскадными фоллбэками
* - инъекция 6 обязательных лейблов (новый стандарт + legacy)
* - фоновая отправка через pcntl_fork (CLI) или синхронная (FPM)
* - гибкая конфигурация: таймауты, auth, SSL, доп. лейблы
*
* @example
* $pusher = PushgatewayClient::create(
* gatewayUrl: 'http://pushgateway:9091',
* serviceName: 'my-php-app',
* );
* $pusher->start();
*
* $counter = $pusher->getRegistry()->getOrRegisterCounter(
* 'app', 'orders_total', 'Total orders', ['status']
* );
* $counter->inc(['success']); // K8s-лейблы добавятся автоматически
*/
class PushgatewayClient
{
// === Новый стандарт именования лейблов ===
public const LABEL_CLUSTER_NAME = 'k8s_cluster_name';
public const LABEL_NAMESPACE_NAME = 'k8s_namespace_name';
public const LABEL_POD_NAME = 'k8s_pod_name';
// === Legacy-лейблы (для обратной совместимости) ===
public const LABEL_OLD_CLUSTER = 'k8s_cluster';
public const LABEL_OLD_NAMESPACE = 'namespace';
public const LABEL_OLD_POD = 'pod';
// === Переменные окружения ===
private const ENV_CLUSTER_NAME = 'K8S_CLUSTER_NAME';
private const ENV_NAMESPACE_NAME = 'K8S_NAMESPACE_NAME';
private const ENV_POD_NAME = 'K8S_POD_NAME';
// === Legacy-переменные (убрать в будущих версиях) ===
private const ENV_LEGACY_CLUSTER = 'CI_ENV';
private const ENV_LEGACY_HOSTNAME = 'HOSTNAME';
// === Системные константы ===
private const K8S_NAMESPACE_FILE = '/var/run/secrets/kubernetes.io/serviceaccount/namespace';
private const VALUE_NOT_AVAILABLE = 'not_available';
// === Настройки по умолчанию ===
private const DEFAULT_PUSH_INTERVAL = 30.0;
private const DEFAULT_HTTP_TIMEOUT = 2.0;
/** @var array<string, string> */
private readonly array $k8sLabels;
private readonly BasePushGateway $gateway;
private readonly CollectorRegistry $registry;
private readonly string $serviceName;
private readonly float $pushIntervalSeconds;
private readonly bool $skipSslVerify;
private ?bool $isRunning = null;
private ?int $pid = null;
/**
* @param BasePushGateway $gateway Экземпляр официального PushGateway-клиента
* @param CollectorRegistry $registry Экземпляр CollectorRegistry для регистрации метрик
* @param string $serviceName Имя сервиса (job name в Pushgateway)
* @param array<string, string> $k8sLabels Предварительно собранные Kubernetes-лейблы
* @param float $pushIntervalSeconds Интервал отправки метрик
* @param bool $skipSslVerify Пропускать проверку SSL (только для dev)
*/
public function __construct(
BasePushGateway $gateway,
CollectorRegistry $registry,
string $serviceName,
array $k8sLabels,
float $pushIntervalSeconds = self::DEFAULT_PUSH_INTERVAL,
bool $skipSslVerify = false,
) {
$this->gateway = $gateway;
$this->registry = $registry;
$this->serviceName = $serviceName;
$this->k8sLabels = $k8sLabels;
$this->pushIntervalSeconds = $pushIntervalSeconds;
$this->skipSslVerify = $skipSslVerify;
}
/**
* Factory-метод с полной конфигурацией.
*
* @param string $gatewayUrl URL Pushgateway
* @param string $serviceName Имя сервиса (job name)
* @param Adapter|null $storageAdapter Адаптер хранилища (по умолчанию APC)
* @param array<string, mixed> $options Опции:
* - push_interval_seconds: float
* - http_timeout_seconds: float
* - auth_token: string|null
* - auth_username: string|null (для basic auth)
* - auth_password: string|null
* - extra_labels: array<string, string>
* - skip_ssl_verify: bool
* - ca_cert_path: string|null (путь к кастомному CA-сертификату)
*/
public static function create(
string $gatewayUrl,
string $serviceName,
?Adapter $storageAdapter = null,
array $options = [],
): self {
$storageAdapter ??= new APC();
$registry = new CollectorRegistry($storageAdapter, false);
$config = array_merge([
'push_interval_seconds' => self::DEFAULT_PUSH_INTERVAL,
'http_timeout_seconds' => self::DEFAULT_HTTP_TIMEOUT,
'auth_token' => null,
'auth_username' => null,
'auth_password' => null,
'extra_labels' => [],
'skip_ssl_verify' => false,
'ca_cert_path' => null,
], $options);
// Собираем K8s-лейблы с фоллбэками
$k8sLabels = self::buildK8sLabels((array) $config['extra_labels']);
// Создаём Guzzle-клиент с нашей конфигурацией
$guzzleConfig = self::buildGuzzleConfig(
timeoutSeconds: (float) $config['http_timeout_seconds'],
authToken: $config['auth_token'],
authUsername: $config['auth_username'],
authPassword: $config['auth_password'],
skipSslVerify: (bool) $config['skip_ssl_verify'],
caCertPath: $config['ca_cert_path'],
);
$httpClient = new GuzzleClient($guzzleConfig);
// Создаём официальный PushGateway-клиент с нашим HTTP-клиентом
$gateway = new BasePushGateway($gatewayUrl, $httpClient);
return new self(
gateway: $gateway,
registry: $registry,
serviceName: $serviceName,
k8sLabels: $k8sLabels,
pushIntervalSeconds: (float) $config['push_interval_seconds'],
skipSslVerify: (bool) $config['skip_ssl_verify'],
);
}
/**
* Возвращает CollectorRegistry для регистрации метрик.
*/
public function getRegistry(): CollectorRegistry
{
return $this->registry;
}
/**
* Возвращает массив текущих Kubernetes-лейблов (для отладки).
*
* @return array<string, string>
*/
public function getK8sLabels(): array
{
return $this->k8sLabels;
}
// ========================================================================
// === Методы получения Kubernetes-метаданных (как в Go-версии) ===
// ========================================================================
private static function getClusterName(): string
{
$value = getenv(self::ENV_CLUSTER_NAME);
if ($value !== false && $value !== '') {
return $value;
}
$legacy = getenv(self::ENV_LEGACY_CLUSTER);
if ($legacy !== false && $legacy !== '') {
return $legacy;
}
return self::VALUE_NOT_AVAILABLE;
}
private static function getNamespace(): string
{
$value = getenv(self::ENV_NAMESPACE_NAME);
if ($value !== false && $value !== '') {
return $value;
}
$content = @file_get_contents(self::K8S_NAMESPACE_FILE);
if ($content !== false) {
return trim($content);
}
return self::VALUE_NOT_AVAILABLE;
}
private static function getPodName(): string
{
$value = getenv(self::ENV_POD_NAME);
if ($value !== false && $value !== '') {
return $value;
}
$legacy = getenv(self::ENV_LEGACY_HOSTNAME);
if ($legacy !== false && $legacy !== '') {
return $legacy;
}
return self::VALUE_NOT_AVAILABLE;
}
/**
* Собирает 6 обязательных Kubernetes-лейблов + пользовательские.
*/
private static function buildK8sLabels(array $extraLabels): array
{
$clusterName = self::getClusterName();
$namespace = self::getNamespace();
$podName = self::getPodName();
$labels = [
// Новый стандарт
self::LABEL_CLUSTER_NAME => $clusterName,
self::LABEL_NAMESPACE_NAME => $namespace,
self::LABEL_POD_NAME => $podName,
// Legacy для обратной совместимости
self::LABEL_OLD_CLUSTER => $clusterName,
self::LABEL_OLD_NAMESPACE => $namespace,
self::LABEL_OLD_POD => $podName,
];
return array_merge($labels, $extraLabels);
}
// ========================================================================
// === Конфигурация HTTP-клиента (Guzzle) ===
// ========================================================================
/**
* Создаёт конфигурацию для Guzzle-клиента.
*
* @return array{timeout: float, headers?: array, verify: bool|string, auth?: array}
*/
private static function buildGuzzleConfig(
float $timeoutSeconds,
?string $authToken,
?string $authUsername,
?string $authPassword,
bool $skipSslVerify,
?string $caCertPath,
): array {
$config = [
'timeout' => $timeoutSeconds,
// verify: false — отключает проверку, строка — путь к CA-сертификату
'verify' => $skipSslVerify ? false : ($caCertPath ?? true),
];
// Bearer-токен
if ($authToken !== null && $authToken !== '') {
$config['headers']['Authorization'] = 'Bearer ' . $authToken;
}
// Basic Auth
if ($authUsername !== null && $authPassword !== null) {
$config['auth'] = [$authUsername, $authPassword];
}
return $config;
}
// ========================================================================
// === Управление отправкой метрик ===
// ========================================================================
/**
* Запускает фоновую периодическую отправку метрик.
*
* @return bool Успешность запуска
*/
public function start(): bool
{
if ($this->isRunning === true) {
return false;
}
if (PHP_SAPI === 'cli' && function_exists('pcntl_fork')) {
$pid = pcntl_fork();
if ($pid === -1) {
error_log('[Pushgateway] Failed to fork background process');
return false;
}
if ($pid > 0) {
$this->isRunning = true;
$this->pid = $pid;
return true;
}
// Дочерний процесс
$this->runPushLoop();
exit(0);
}
// Fallback: одна отправка (для FPM)
$this->pushOnce();
$this->isRunning = true;
return true;
}
/**
* Останавливает фоновую отправку.
*/
public function stop(): void
{
if ($this->pid !== null && $this->isRunning === true) {
if (function_exists('posix_kill')) {
posix_kill($this->pid, SIGTERM);
}
$this->isRunning = false;
$this->pid = null;
}
}
/**
* Немедленно отправляет метрики в Pushgateway (синхронно).
*
* @throws RuntimeException при ошибке отправки
*/
public function pushOnce(): void
{
// Официальная библиотека принимает лейблы как третий аргумент
// Формат: ['label_name' => 'label_value', ...]
$error = $this->gateway->push($this->registry, $this->serviceName, $this->k8sLabels);
if ($error !== null) {
throw new RuntimeException(sprintf(
'Pushgateway error: %s',
$error
));
}
}
/**
* Основной цикл фоновой отправки.
*/
private function runPushLoop(): void
{
$this->isRunning = true;
$this->pushOnce();
while ($this->isRunning) {
sleep((int) $this->pushIntervalSeconds);
if ($this->pid !== null && function_exists('posix_kill')) {
if (posix_kill($this->pid, 0) === false) {
break;
}
}
$this->pushOnce();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment