Skip to content

Commit

Permalink
Introduce Terminus metrics command. (#1835)
Browse files Browse the repository at this point in the history
  • Loading branch information
greg-1-anderson authored Mar 20, 2018
1 parent 9c0e18a commit d37b351
Show file tree
Hide file tree
Showing 11 changed files with 804 additions and 12 deletions.
22 changes: 21 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ The Terminus 1.x unit tests can be run via:

### Functional Tests

The functional test files are in the `tests/features` directory. To run the entire test suite for Terminus 0.x:
The functional test files are in the `tests/features` directory. Any test which touches the backed is mocked with [VCR](http://php-vcr.github.io).

#### Running existing tests

To run the entire test suite for Terminus 0.x:

`vendor/bin/behat -c=tests/config/behat.yml`

Expand All @@ -92,6 +96,22 @@ The functional test files for the new version of Terminus are in the `tests/acti

More information can be found by running `vendor/bin/behat --help`.

#### Recording new tests

To record a new test, configure the `parameters` section of the file [tests/config/behat.yml](tests/config/behat.yml) as follows:
```
parameters:
user_id: '[[YOUR-USER-ID-HERE]]'
username: '[[YOUR-EMAIL-ADDRESS-HERE]]'
host: 'terminus.pantheon.io:443'
vcr_mode: 'new_episodes'
machine_token: '[[YOUR-MACHINE-TOKEN-HERE]]'
```
Then, run a single test as described above. VCR will then call the backend and record the results received in the specified .yml file. This is done for any Behat scenario labeled `@vcr filename.yml`. Pick a filename appropriate for the test.

Once the VCR .yml file has been saved, you may restore your behat.yml configuration file to its previous state (at a minimum, set `vcr_mode` back to `none`). Subsequent test runs will pull data from the VCR .yml file to satisfy future web requests.

You may need to add yourself to the [team of the behat-tests site](https://admin.dashboard.pantheon.io/sites/e885f5fe-6644-4df6-a292-68b2b57c33ad#dev/code) (Pantheon employees) or use a different test site. Once you have captured the events you would like to record, hand-sanitize them of any sensitive information such as machine tokens and bearer authorization headers.

Versioning
----------
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"require": {
"php": ">=5.5.9",
"composer/semver": "^1.4",
"consolidation/output-formatters": "^3.2",
"consolidation/robo": "^1.1.0",
"guzzlehttp/guzzle": "^6.2",
"psy/psysh": "^0.8",
Expand All @@ -32,7 +33,7 @@
"bin/terminus"
],
"scripts": {
"behat": "COMPOSER_PROCESS_TIMEOUT=3600 SHELL_INTERACTIVE=true behat --colors -c=tests/config/behat.yml --suite=default",
"behat": "SHELL_INTERACTIVE=true behat --colors -c=tests/config/behat.yml --suite=default",
"cbf": "phpcbf --standard=PSR2 -n tests/unit_tests/* bin/terminus src/*",
"clover": "phpunit -c tests/config/phpunit.xml.dist --coverage-clover tests/logs/clover.xml",
"coveralls": "coveralls -v -c tests/config/coveralls.xml",
Expand All @@ -51,6 +52,7 @@
"squizlabs/php_codesniffer": "^2.7"
},
"config": {
"process-timeout": 3600,
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
Expand Down
22 changes: 14 additions & 8 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions src/Collections/APICollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,20 @@ public function getUrl()
* @return array
*/
protected function requestData()
{
return $this->requestDataAtUrl($this->getUrl(), $this->getFetchArgs());
}

/**
* Make a request at a specific URL
* @param string $url address to fetch
* @param array $args request arguments (@see APICollection::getFetchArgs())
* @return array
*/
protected function requestDataAtUrl($url, $args = [])
{
$default_args = ['options' => ['method' => 'get',],];
$args = array_merge($default_args, $this->getFetchArgs());
$url = $this->getUrl();
$args = array_merge($default_args, $args);

if ($this->isPaged()) {
$results = $this->request()->pagedRequest($url, $args);
Expand Down
207 changes: 207 additions & 0 deletions src/Collections/Metrics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php

namespace Pantheon\Terminus\Collections;

use Pantheon\Terminus\Models\Metric;

/**
* Class Metrics
* @package Pantheon\Terminus\Collections
*/
class Metrics extends EnvironmentOwnedCollection
{
public static $pretty_name = 'metrics';

/**
* @var string
*/
protected $collected_class = Metric::class;

/**
* @var string base URL to fetch
*/
protected $url = 'sites/{site_id}/environments/{environment_id}/{series}?granularity={period}&datapoints={datapoints}';

/**
* @var array
*/
protected $metadata;

/**
* @var string The period of data to fix (month/week/day)
*/
protected $period;

/**
* @var string The number of data points to fetch (28/12 max)
*/
protected $datapoints;

/**
* Metrics constructor
*/
public function __constructor()
{
}

/**
* @return string
*/
public function getPeriod()
{
return $this->period;
}

/**
* @param string $value
*/
public function setPeriod($value)
{
return $this->setParameter('period', $value);
}

/**
* @return string
*/
public function getDatapoints()
{
return $this->datapoints;
}

/**
* @param string $value
*/
public function setDatapoints($value)
{
return $this->setParameter('datapoints', $value);
}

/**
* @param string $value
*/
protected function setParameter($parameter, $value)
{
if ($this->$parameter === $value) {
return $this;
}
// Clear our cache whenever our parameters are changed
$this->setData([]);
$this->$parameter = $value;
return $this;
}

/**
* Our API returns our data series inside a 'timeseries' element.
* To be compatible with Terminus' data model, we need to unwrap
* this and return an array of data items. We also convert the
* timestamp from seconds to the date format used elsewhere in Terminus.
*/
protected function requestData()
{
$rawPagesServed = $this->requestDataAtUrl($this->getUrlForSeries('pageviews'), $this->getFetchArgs());

if (empty($rawPagesServed->timeseries)) {
throw new \Exception("No data available.");
}

$rawVisits = $this->requestDataAtUrl($this->getUrlForSeries('visits'), $this->getFetchArgs());

// The data is passed to us with the data series of primary
// interest to us nested inside a 'timeseries' element. The
// requirements for an EnvironmentOwnedCollection or any
// TerminusCollection is that the request data must return
// a list of all of our data items.
// (@see TerminusCollection::fetch())
// We also need to ensure that the elements of our timeseries
// have unique IDs. We will use the time value for this.
$pageviewData = $this->assignIds($rawPagesServed->timeseries, 'timestamp');
$visitData = $this->assignIds($rawVisits->timeseries, 'timestamp');
$combineddata = $this->combineRawData($pageviewData, $visitData);

// Convert the timestamp to a datetime. The timestamp will remain
// as the row key.
$data = array_map(
function ($item) {
$item->datetime = gmdate("Y-m-d\TH:i:s", $item->timestamp);
unset($item->timestamp);
return $item;
},
$combineddata
);

// Our parent class is already caching our data series; we will
// store the other items in a 'metadata' field. We will avoid
// caching the raw data because that would duplicate data
// already cached.
unset($rawPagesServed->timeseries);
$this->metadata = $rawPagesServed;

return $data;
}

/**
* Combine the raw pages served data with the raw unique visits data
* @param array $rawPagesServed
* @param array $rawVisits
* @return array
*/
protected function combineRawData($rawPagesServed, $rawVisits)
{
$result = $rawVisits;
foreach ($result as $time => $item) {
$item->visits = $item->value;
$result[$time]->pages_served = $rawPagesServed[$time]->value;
unset($result[$time]->value);
}
return $result;
}

/**
* When serializing the metrics again, wrap the timeseries data
* back inside a 'timeseries' element and then union in the metadata.
* @return array
*/
public function serialize()
{
$timeseries = parent::serialize();
return (array) $this->metadata + ['timeseries' => $timeseries];
}

/**
* Convert an array with numeric indexes to an associative array whose
* indexes are taken from one of the data elements.
* @param $data An array of items with numeric indexes
* @param $keyId The id of the element in each item that is the key
*
* @return associative array of the same input items with new keys
*/
protected function assignIds($data, $keyId)
{
// Return an array consisting of all of the values of
// the data column identified by $keyId.
$keys = array_map(
function ($item) use ($keyId) {
return $item->$keyId;
},
$data
);

return array_combine($keys, $data);
}

/**
* Fill in the parameters for the desired request.
* @param string $seriesId
* @return string
*/
protected function getUrlForSeries($seriesId)
{
$url = $this->getUrl();
$tr = [
'{series}' => $seriesId,
'{period}' => $this->getPeriod(),
'{datapoints}' => $this->getDatapoints(),
];
return strtr($url, $tr);
}
}
Loading

0 comments on commit d37b351

Please sign in to comment.