Skip to content

Commit

Permalink
feat(*): Add methods to build and push usage metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
julienloizelet committed Dec 20, 2024
1 parent a3041cf commit 12bd494
Show file tree
Hide file tree
Showing 15 changed files with 782 additions and 52 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ As far as possible, we try to adhere to [Symfony guidelines](https://symfony.com

---

## [4.0.0](https://github.com/crowdsecurity/php-lapi-client/releases/tag/v4.0.0) - 202?-??-??
[_Compare with previous release_](https://github.com/crowdsecurity/php-lapi-client/compare/v3.3.2...HEAD)

**This release is not yet published**

### Added

- Add `pushUsageMetrics` method to `Bouncer` class

### Changed

- *Breaking change*: Move configuration classes to `CrowdSec\LapiClient\Configuration` namespace

---

## [3.3.2](https://github.com/crowdsecurity/php-lapi-client/releases/tag/v3.3.2) - 2024-10-21
[_Compare with previous release_](https://github.com/crowdsecurity/php-lapi-client/compare/v3.3.1...v3.3.2)

Expand Down
30 changes: 30 additions & 0 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
- [Get AppSec decision](#get-appsec-decision)
- [Command usage](#command-usage-2)
- [Example](#example-1)
- [Push usage metrics](#push-usage-metrics)
- [Command usage](#command-usage-3)
- [Example](#example-2)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand All @@ -60,6 +63,7 @@ This client allows you to interact with the CrowdSec Local API (LAPI).
- Retrieve decisions stream list
- Retrieve decisions for some filter
- Retrieve AppSec decision
- Push usage metrics
- Overridable request handler (`curl` by default, `file_get_contents` also available)


Expand Down Expand Up @@ -151,6 +155,18 @@ The `$rawBody` parameter is optional and must be used if the forwarded request c

Please see the [CrowdSec AppSec documentation](https://docs.crowdsec.net/docs/appsec/intro/) for more details.

##### Push usage metrics

To push usage metrics, you can do the following call:

```php
$client->pushUsageMetrics($usageMetrics);
```

The `$usageMetrics` parameter is an array containing the usage metrics to push. Please see the [CrowdSec LAPI documentation](https://crowdsecurity.github.io/api_doc/index.html?urls.primaryName=LAPI#/bouncers/postUsageMetrics) for more details.

We provide a `buildUsageMetrics` method to help you build the `$usageMetrics` array.


## Bouncer client configurations

Expand Down Expand Up @@ -521,3 +537,17 @@ php tests/scripts/bouncer/appsec-decision.php <BOUNCER_KEY> <HEADERS_JSON> <APPS
```bash
php tests/scripts/bouncer/appsec-decision.php 'KWurslwIaE2aZSZjYU9mQAWTFb6AHiPTFNTsYTZvoAU' '{"X-Crowdsec-Appsec-Ip":"1.2.3.4","X-Crowdsec-Appsec-Uri":"/login","X-Crowdsec-Appsec-Host":"example.com","X-Crowdsec-Appsec-Verb":"POST","X-Crowdsec-Appsec-User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0"}' http://crowdsec:7422 'class.module.classLoader.resources.'
```

### Push usage metrics

#### Command usage

```bash
php tests/scripts/bouncer/build-and-push-metrics.php <METRICS_JSON> <BOUNCER_KEY> <LAPI_URL>
```

#### Example

```bash
php tests/scripts/bouncer/build-and-push-metrics.php '{"name":"TEST BOUNCER","type":"crowdsec-test-php-bouncer","version":"v0.0.0","items":[{"name":"dropped","value":12,"unit":"request","labels":{"origin":"CAPI"}}],"meta":{"window_size_seconds":900,"utc_now_timestamp":12}}' 92d3de1dde6d354b771d63035cf5ef83 https://crowdsec:8080
```
99 changes: 99 additions & 0 deletions src/Bouncer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use CrowdSec\Common\Client\ClientException as CommonClientException;
use CrowdSec\Common\Client\RequestHandler\RequestHandlerInterface;
use CrowdSec\Common\Client\TimeoutException as CommonTimeoutException;
use CrowdSec\LapiClient\Configuration\Bouncer as Configuration;
use Psr\Log\LoggerInterface;
use Symfony\Component\Config\Definition\Processor;

Expand Down Expand Up @@ -45,6 +46,79 @@ public function __construct(
parent::__construct($this->configs, $requestHandler, $logger);
}

/**
* Helper to create well formatted metrics array.
*
* @param array $properties
* Array containing metrics properties
* $properties = [
* 'name' => (string) Bouncer name
* 'type' => (string) Bouncer type (crowdsec-php-bouncer)
* 'last_pull' => (integer) last pull timestamp,
* 'version' => (string) Bouncer version
* 'feature_flags' => (array) Should be empty for bouncer
* 'utc_startup_timestamp' => (integer) Bouncer startup timestamp
* 'os' => (array) OS information
* 'os' = [
* 'name' => (string) OS name
* 'version' => (string) OS version
* ]
* ];
* @param array $meta
* Array containing meta data
* $meta = [
* 'window_size_seconds' => (integer) Window size in seconds
* 'utc_now_timestamp' => (integer) Current timestamp
* ];
* @param array[] $items
* Array of items. Each item is an array too.
* $items = [
* [
* 'name' => (string) Name of the metric
* 'value' => (integer) Value of the metric
* 'type' => (string) Type of the metric
* 'labels' => (array) Labels of the metric
* 'labels' = [
* 'key' => (string) Tag key
* 'value' => (string) Tag value
* ],
* ],
* ...
* ]
*
* @throws ClientException
*/
public function buildUsageMetrics(array $properties, array $meta, array $items = [[]]): array
{
$finalProperties = [
'name' => $properties['name'] ?? '',
'type' => $properties['type'] ?? Constants::METRICS_TYPE,
'version' => $properties['version'] ?? '',
'feature_flags' => $properties['feature_flags'] ?? [],
'utc_startup_timestamp' => $properties['utc_startup_timestamp'] ?? 0,
];
$lastPull = $properties['last_pull'] ?? 0;
$os = $properties['os'] ?? $this->getOs();
if ($lastPull) {
$finalProperties['last_pull'] = $lastPull;
}
if (!empty($os['name']) && !empty($os['version'])) {
$finalProperties['os'] = $os;
}
$meta = [
'window_size_seconds' => $meta['window_size_seconds'] ?? 0,
'utc_now_timestamp' => $meta['utc_now_timestamp'] ?? time(),
];

try {
$metrics = new Metrics($finalProperties, $meta, $items);
} catch (\Exception $e) {
throw new ClientException('Something went wrong while creating metrics: ' . $e->getMessage());
}

return $metrics->toArray();
}

/**
* Process a call to AppSec component.
*
Expand Down Expand Up @@ -99,6 +173,23 @@ public function getStreamDecisions(
);
}

/**
* Push usage metrics to LAPI.
*
* @see https://crowdsecurity.github.io/api_doc/index.html?urls.primaryName=LAPI#/Remediation%20component/usage-metrics
*
* @throws ClientException
* @codeCoverageIgnore
*/
public function pushUsageMetrics(array $usageMetrics): array
{
return $this->manageRequest(
'POST',
Constants::METRICS_ENDPOINT,
$usageMetrics
);
}

private function cleanHeadersForLog(array $headers): array
{
$cleanedHeaders = $headers;
Expand Down Expand Up @@ -136,6 +227,14 @@ private function formatUserAgent(array $configs = []): string
return Constants::USER_AGENT_PREFIX . $userAgentSuffix . '/' . $userAgentVersion;
}

private function getOs(): array
{
return [
'name' => php_uname('s'),
'version' => php_uname('v'),
];
}

/**
* Make a request to the AppSec component of LAPI.
*
Expand Down
41 changes: 21 additions & 20 deletions src/Configuration.php → src/Configuration/Bouncer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

declare(strict_types=1);

namespace CrowdSec\LapiClient;
namespace CrowdSec\LapiClient\Configuration;

use CrowdSec\Common\Configuration\AbstractConfiguration;
use CrowdSec\LapiClient\Constants;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
Expand All @@ -19,7 +20,7 @@
* @copyright Copyright (c) 2022+ CrowdSec
* @license MIT License
*/
class Configuration extends AbstractConfiguration
class Bouncer extends AbstractConfiguration
{
/** @var array<string> The list of each configuration tree key */
protected $keys = [
Expand Down Expand Up @@ -79,6 +80,24 @@ public function getConfigTreeBuilder(): TreeBuilder
return $treeBuilder;
}

/**
* AppSec settings.
*
* @param NodeDefinition|ArrayNodeDefinition $rootNode
*
* @return void
*
* @throws \InvalidArgumentException
*/
private function addAppSecNodes($rootNode)
{
$rootNode->children()
->scalarNode('appsec_url')->cannotBeEmpty()->defaultValue(Constants::DEFAULT_APPSEC_URL)->end()
->integerNode('appsec_timeout_ms')->defaultValue(Constants::APPSEC_TIMEOUT_MS)->end()
->integerNode('appsec_connect_timeout_ms')->defaultValue(Constants::APPSEC_CONNECT_TIMEOUT_MS)->end()
->end();
}

/**
* LAPI connection settings.
*
Expand Down Expand Up @@ -117,24 +136,6 @@ private function addConnectionNodes($rootNode)
->end();
}

/**
* AppSec settings.
*
* @param NodeDefinition|ArrayNodeDefinition $rootNode
*
* @return void
*
* @throws \InvalidArgumentException
*/
private function addAppSecNodes($rootNode)
{
$rootNode->children()
->scalarNode('appsec_url')->cannotBeEmpty()->defaultValue(Constants::DEFAULT_APPSEC_URL)->end()
->integerNode('appsec_timeout_ms')->defaultValue(Constants::APPSEC_TIMEOUT_MS)->end()
->integerNode('appsec_connect_timeout_ms')->defaultValue(Constants::APPSEC_CONNECT_TIMEOUT_MS)->end()
->end();
}

/**
* Conditional validation.
*
Expand Down
65 changes: 65 additions & 0 deletions src/Configuration/Metrics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace CrowdSec\LapiClient\Configuration;

use CrowdSec\Common\Configuration\AbstractConfiguration;
use CrowdSec\LapiClient\Constants;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;

/**
* The LAPI client metrics properties configuration.
*
* @author CrowdSec team
*
* @see https://crowdsec.net CrowdSec Official Website
*
* @copyright Copyright (c) 2022+ CrowdSec
* @license MIT License
*/
class Metrics extends AbstractConfiguration
{
/** @var array<string> The list of each configuration tree key */
protected $keys = [
'name',
'type',
'last_pull',
'version',
'os',
'feature_flags',
'utc_startup_timestamp',
];

/**
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('metricsConfig');
/** @var ArrayNodeDefinition $rootNode */
$rootNode = $treeBuilder->getRootNode();
$rootNode->children()
->scalarNode('name')->isRequired()->cannotBeEmpty()->end()
->scalarNode('type')->isRequired()->defaultValue(Constants::METRICS_TYPE)->end()
->integerNode('last_pull')->end()
->scalarNode('version')->isRequired()->cannotBeEmpty()->end()
->integerNode('utc_startup_timestamp')->isRequired()->min(0)->end()
->arrayNode('os')
->children()
->scalarNode('name')->isRequired()->cannotBeEmpty()->end()
->scalarNode('version')->isRequired()->cannotBeEmpty()->end()
->end()
->end()
->arrayNode('feature_flags')
->scalarPrototype()->end()
->defaultValue([])
->end()
->end()
;

return $treeBuilder;
}
}
Loading

0 comments on commit 12bd494

Please sign in to comment.