diff --git a/README.md b/README.md index e059aff..35eda11 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ Targets to provide simple means for obtaining data from Prometheus API. -Stable for [Prometheus 1.x and <= v2.0 api spec](https://prometheus.io/docs/prometheus/2.0/querying/api/) +Stable for [Prometheus 1.x and <= v2.1 api spec](https://prometheus.io/docs/prometheus/2.1/querying/api/) +NOTICE: some endpoints are only available in newer versions of Prometheus. For detailed list see [table of available calls](#available-calls) below. ## Instalation Use composer to add PApi as dependency: @@ -43,15 +44,15 @@ Use composer to add PApi as dependency: ### Available calls PApi currently has methods for all available endpoints provided by Prometheus. -#### Query - $client->getQuery('up', new \DateTimeImmutable('now'); -#### QueryRange - $client->getQueryRange('up', new \DateTimeImmutable('today'), new \DateTimeImmutable('now'), '12h'); -#### Series - $client->getSeries(['up'], new \DateTimeImmutable('-1 minute'), new \DateTimeImmutable('now')); -#### Label Values - $client->getLabelValues('job'); -#### Targets (active only) - $client->getTargets(); -#### Alert Managers - $client->getAlertManagers(); + +| Call | Code | Prometheus compatibility | Official doc | +| --------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------ | +| Query | `$client->getQuery('up', new \DateTimeImmutable('now');` | >=1.0 | [doc](https://prometheus.io/docs/prometheus/2.1/querying/api/#instant-queries) | +| QueryRange | `$client->getQueryRange('up', new \DateTimeImmutable('today'), new \DateTimeImmutable('now'), '12h');` | >=1.0 | [doc](https://prometheus.io/docs/prometheus/2.1/querying/api/#range-queries) | +| Series | `$client->getSeries(['up'], new \DateTimeImmutable('-1 minute'), new \DateTimeImmutable('now');` | >=1.0 | [doc](https://prometheus.io/docs/prometheus/2.1/querying/api/#finding-series-by-label-matchers) | +| Label Values | `$client->getLabelValues('job');` | >=1.0 | [doc](https://prometheus.io/docs/prometheus/2.1/querying/api/#querying-label-values) | +| Targets (active only) | `$client->getTargets();` | >=1.0 | [doc](https://prometheus.io/docs/prometheus/2.1/querying/api/#targets) | +| Alert Managers | `$client->getAlertManagers();` | >=1.0 | [doc](https://prometheus.io/docs/prometheus/2.1/querying/api/#alertmanagers) | +| Create snapshot | `$client->createSnapshot();` | >=2.1 | [doc](https://prometheus.io/docs/prometheus/2.1/querying/api/#snapshot) | +| Delete series | `$client->deleteSeries(['up'], new DateTimeImmutable('today'), new DateTimeImmutable('now');` | >=2.1 | [doc](https://prometheus.io/docs/prometheus/2.1/querying/api/#delete-series) | +| Clean tombstones | `$client->cleanTombstones();` | >=2.1 | [doc](https://prometheus.io/docs/prometheus/2.1/querying/api/#clean-tombstones) | diff --git a/composer.json b/composer.json index c21e2d6..f72b8f0 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ }, "require-dev": { "cdn77/coding-standard": "^0.5", + "guzzlehttp/guzzle": "^6.3", "phpstan/phpstan": "^0.9", "phpstan/phpstan-strict-rules": "^0.9", "phpunit/phpunit": "^6.3", diff --git a/src/Client.php b/src/Client.php index ac799a2..77503d9 100644 --- a/src/Client.php +++ b/src/Client.php @@ -48,18 +48,18 @@ public function connect() : void public function getAlertManagers() : DataResponse { - return DataResponseMeta::fromJson($this->connection->execute('alertmanagers')); + return DataResponseMeta::fromJson($this->connection->executeGet('alertmanagers')); } public function getTargets() : DataResponse { - return DataResponseMeta::fromJson($this->connection->execute('targets')); + return DataResponseMeta::fromJson($this->connection->executeGet('targets')); } public function getLabelValues(string $label) : ArrayValuesResponse { return ArrayValuesResponseMeta::fromJson( - $this->connection->execute('label/' . urlencode($label) . '/values') + $this->connection->executeGet('label/' . urlencode($label) . '/values') ); } @@ -69,7 +69,7 @@ public function getLabelValues(string $label) : ArrayValuesResponse public function getSeries(array $match, \DateTimeInterface $start, \DateTimeInterface $end) : ArrayValuesResponse { return ArrayValuesResponseMeta::fromJson( - $this->connection->execute('series', [ + $this->connection->executeGet('series', [ 'match' => array_values($match), 'start' => $start->format(self::DATETIME_FORMAT), 'end' => $end->format(self::DATETIME_FORMAT), @@ -89,7 +89,7 @@ public function getQueryRange( } return DataResponseMeta::fromJson( - $this->connection->execute('query_range', [ + $this->connection->executeGet('query_range', [ 'query' => $query, 'start' => $start->format(self::DATETIME_FORMAT), 'end' => $end->format(self::DATETIME_FORMAT), @@ -109,11 +109,42 @@ public function getQuery( } return DataResponseMeta::fromJson( - $this->connection->execute('query', [ + $this->connection->executeGet('query', [ 'query' => $query, 'time' => $time->format(self::DATETIME_FORMAT), 'timeout' => $timeout, ]) ); } + + /** + * Make sure to enable admin APIs via `--web.enable-admin-api` + */ + public function createSnapshot() : DataResponse + { + return DataResponseMeta::fromJson($this->connection->executePost('admin/tsdb/snapshot')); + } + + /** + * Make sure to enable admin APIs via `--web.enable-admin-api` + * @param string[] $match + */ + public function deleteSeries(array $match, \DateTimeInterface $start, \DateTimeInterface $end) : bool + { + $this->connection->executePost('admin/tsdb/delete_series', [ + 'match' => array_values($match), + 'start' => $start->format(self::DATETIME_FORMAT), + 'end' => $end->format(self::DATETIME_FORMAT), + ]); + return true; + } + + /** + * Make sure to enable admin APIs via `--web.enable-admin-api` + */ + public function cleanTombstones() : bool + { + $this->connection->executePost('admin/tsdb/clean_tombstones'); + return true; + } } diff --git a/src/Connection/ConnectionInterface.php b/src/Connection/ConnectionInterface.php index b368932..ef1a615 100644 --- a/src/Connection/ConnectionInterface.php +++ b/src/Connection/ConnectionInterface.php @@ -19,5 +19,10 @@ public function create(array $config) : self; /** * @param mixed[] $query */ - public function execute(string $endPoint, array $query = []) : string; + public function executeGet(string $endPoint, array $query = []) : string; + + /** + * @param mixed[] $query + */ + public function executePost(string $endPoint, array $query = []) : string; } diff --git a/src/Connection/CurlConnection.php b/src/Connection/CurlConnection.php index a3f8f2f..ced282a 100644 --- a/src/Connection/CurlConnection.php +++ b/src/Connection/CurlConnection.php @@ -34,27 +34,38 @@ public function create(array $config) : ConnectionInterface /** * @param mixed[] $query */ - public function execute(string $endPoint, array $query = []) : string + public function executeGet(string $endPoint, array $query = []) : string { if ($this->connection === null) { $this->connect(); } - curl_reset($this->connection); + $url = $this->prepareConnection($endPoint, $query); - $queryParameters = http_build_query($query); - $queryString = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '%5B%5D=', $queryParameters); - $uriParts = $this->getBaseUriParts(); - $uriParts['path'] .= $endPoint; - $uriParts['query'] = $queryString; - $url = $this->buildUrl($uriParts); + $response = curl_exec($this->connection); + if ($response === false) { + throw new ConnectionException( + sprintf( + 'Request GET %s returned with code %d and message `%s`', + $url, + curl_getinfo($this->connection, CURLINFO_RESPONSE_CODE), + curl_error($this->connection) + ) + ); + } - curl_setopt($this->connection, CURLOPT_URL, $url); - curl_setopt($this->connection, CURLOPT_TIMEOUT, $this->config['timeout']); - curl_setopt($this->connection, CURLOPT_RETURNTRANSFER, true); - $curlHeaders = array_map(function ($key, $value) { - return $key . ':' . $value; - }, $this->config['connectionHeaders']); - curl_setopt($this->connection, CURLOPT_HTTPHEADER, $curlHeaders); + return $response; + } + + /** + * @param mixed[] $query + */ + public function executePost(string $endPoint, array $query = []) : string + { + if ($this->connection === null) { + $this->connect(); + } + $url = $this->prepareConnection($endPoint, $query); + curl_setopt($this->connection, CURLOPT_POST, 1); $response = curl_exec($this->connection); if ($response === false) { @@ -117,4 +128,30 @@ private function getBaseUriParts() : array 'pass' => $this->config['password'], ]; } + + /** + * @param mixed[] $query + */ + protected function prepareConnection(string $endPoint, array $query = []) : string + { + curl_reset($this->connection); + + $uriParts = $this->getBaseUriParts(); + $uriParts['path'] .= $endPoint; + if (!empty($query)) { + $queryParameters = http_build_query($query); + $queryString = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '%5B%5D=', $queryParameters); + $uriParts['query'] = $queryString; + } + $url = $this->buildUrl($uriParts); + + curl_setopt($this->connection, CURLOPT_URL, $url); + curl_setopt($this->connection, CURLOPT_TIMEOUT, $this->config['timeout']); + curl_setopt($this->connection, CURLOPT_RETURNTRANSFER, true); + $curlHeaders = array_map(function ($key, $value) { + return $key . ':' . $value; + }, $this->config['connectionHeaders']); + curl_setopt($this->connection, CURLOPT_HTTPHEADER, $curlHeaders); + return $url; + } } diff --git a/src/Connection/GuzzleConnection.php b/src/Connection/GuzzleConnection.php index 39ab221..3a1e527 100644 --- a/src/Connection/GuzzleConnection.php +++ b/src/Connection/GuzzleConnection.php @@ -49,7 +49,23 @@ public function create(array $config) : ConnectionInterface /** * @param mixed[] $query */ - public function execute(string $endPoint, array $query = []) : string + public function executeGet(string $endPoint, array $query = []) : string + { + return $this->executeRequest('GET', $endPoint, $query); + } + + /** + * @param mixed[] $query + */ + public function executePost(string $endPoint, array $query = []) : string + { + return $this->executeRequest('POST', $endPoint, $query); + } + + /** + * @param mixed[] $query + */ + private function executeRequest(string $method, string $endPoint, array $query = []) : string { if ($this->client === null) { $this->connect(); @@ -60,21 +76,22 @@ public function execute(string $endPoint, array $query = []) : string $queryString = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '%5B%5D=', $queryParameters); $response = $this->client->request( - 'GET', + $method, $endPoint, [ 'query' => $queryString, ] ); - if ($response->getStatusCode() !== 200) { + if ($response->getStatusCode() !== 200 && $response->getStatusCode() !== 204) { $parts = parse_url($this->getBaseUri()); $parts['path'] = $parts['path'] . $endPoint; $parts['query'] = http_build_query($query); $uri = Uri::fromParts($parts); throw new ConnectionException( sprintf( - 'Request GET `%s` returned with code %d and message `%s`', + 'Request %s `%s` returned with code %d and message `%s`', + $method, (string) $uri, $response->getStatusCode(), $response->getBody()->getContents() diff --git a/src/PApiMetaSpec.php b/src/PApiMetaSpec.php index 861d2f5..f3d4c76 100644 --- a/src/PApiMetaSpec.php +++ b/src/PApiMetaSpec.php @@ -19,7 +19,7 @@ protected function configure() : void $jsonModule = new JsonModule(); $phpModule->addPropertySerializer(new DateTimeFormattingSerializer( - 'Y-m-d\TH:i:s.uP', + 'Y-m-d\TH:i:s.u???P', \DateTimeImmutable::class, '0001-01-01T00:00:00Z' )); diff --git a/src/Response/Meta/ResponseDataMeta.php b/src/Response/Meta/ResponseDataMeta.php index 1d9fd4d..9bac24c 100644 --- a/src/Response/Meta/ResponseDataMeta.php +++ b/src/Response/Meta/ResponseDataMeta.php @@ -25,6 +25,7 @@ class ResponseDataMeta extends ResponseData implements MetaInterface, PhpMetaInt const RESULT = 'result'; const ACTIVE_TARGETS = 'activeTargets'; const ACTIVE_ALERTMANAGERS = 'activeAlertmanagers'; + const NAME = 'name'; /** @var ResponseDataMeta */ private static $instance; @@ -113,6 +114,7 @@ public static function reset($object) $object->result = NULL; $object->activeTargets = NULL; $object->activeAlertmanagers = NULL; + $object->name = NULL; } @@ -159,6 +161,11 @@ public static function hash($object, $algoOrCtx = 'md5', $raw = false) } } + if (isset($object->name)) { + hash_update($ctx, 'name'); + hash_update($ctx, (string)$object->name); + } + if (is_string($algoOrCtx)) { return hash_final($ctx, $raw); } else { @@ -266,6 +273,17 @@ public static function fromArray($input, $group = null, $object = null) $object->activeAlertmanagers = null; } + if (($id & 1) > 0 && isset($input['name'])) { + $object->name = $input['name']; + } elseif (($id & 1) > 0 && array_key_exists('name', $input) && $input['name'] === null) { + $object->name = null; + } + if (($id & 2) > 0 && isset($input['name'])) { + $object->name = $input['name']; + } elseif (($id & 2) > 0 && array_key_exists('name', $input) && $input['name'] === null) { + $object->name = null; + } + return $object; } @@ -367,6 +385,13 @@ public static function toArray($object, $group = null, $filter = null) } } + if (($id & 1) > 0 && ($filter === null || isset($filter['name']))) { + $output['name'] = $object->name; + } + if (($id & 2) > 0 && ((isset($object->name) && $filter === null) || isset($filter['name']))) { + $output['name'] = $object->name; + } + } catch (\Exception $e) { Stack::$objects->detach($object); throw $e; @@ -478,6 +503,17 @@ public static function fromObject($input, $group = null, $object = null) $object->activeAlertmanagers = null; } + if (($id & 1) > 0 && isset($input['name'])) { + $object->name = $input['name']; + } elseif (($id & 1) > 0 && array_key_exists('name', $input) && $input['name'] === null) { + $object->name = null; + } + if (($id & 2) > 0 && isset($input['name'])) { + $object->name = $input['name']; + } elseif (($id & 2) > 0 && array_key_exists('name', $input) && $input['name'] === null) { + $object->name = null; + } + return $object; } @@ -579,6 +615,13 @@ public static function toObject($object, $group = null, $filter = null) } } + if (($id & 1) > 0 && ($filter === null || isset($filter['name']))) { + $output['name'] = $object->name; + } + if (($id & 2) > 0 && ((isset($object->name) && $filter === null) || isset($filter['name']))) { + $output['name'] = $object->name; + } + } catch (\Exception $e) { Stack::$objects->detach($object); throw $e; diff --git a/src/Response/ResponseData.php b/src/Response/ResponseData.php index 840c8dd..5841d6e 100644 --- a/src/Response/ResponseData.php +++ b/src/Response/ResponseData.php @@ -23,6 +23,9 @@ class ResponseData /** @var AlertManager[] */ protected $activeAlertmanagers; + /** @var string */ + protected $name; + public function getResultType() : string { return $this->resultType; @@ -49,4 +52,9 @@ public function getActiveAlertmanagers() : array { return $this->activeAlertmanagers; } + + public function getName() : string + { + return $this->name; + } } diff --git a/tests/Fixtures/snapshotResponse.json b/tests/Fixtures/snapshotResponse.json new file mode 100644 index 0000000..4e2e691 --- /dev/null +++ b/tests/Fixtures/snapshotResponse.json @@ -0,0 +1,6 @@ +{ + "status": "success", + "data": { + "name": "20171210T211224Z-2be650b6d019eb54" + } +} diff --git a/tests/PApi/ResponseTranslationTest.php b/tests/PApi/ResponseTranslationTest.php index 1821461..575017f 100644 --- a/tests/PApi/ResponseTranslationTest.php +++ b/tests/PApi/ResponseTranslationTest.php @@ -96,4 +96,13 @@ public function testVectorResponse() : void $this->assertInstanceOf(Metric::class, $metric); $this->assertInstanceOf(\DateTimeImmutable::class, $metric->getTimeAsObject()); } + + public function testSnapshotResponse() : void + { + $response = DataResponseMeta::fromJson(file_get_contents(__DIR__ . '/../Fixtures/snapshotResponse.json')); + + $this->assertSame(Response::STATUS_SUCCESS, $response->getStatus()); + $this->assertInstanceOf(ResponseData::class, $response->getData()); + $this->assertEquals('20171210T211224Z-2be650b6d019eb54', $response->getData()->getName()); + } }