Merge pull request #1582 from CachetHQ/metric-point-enhancement

Refactoring metric point storage
This commit is contained in:
Graham Campbell
2016-04-25 20:42:11 +01:00
25 changed files with 329 additions and 104 deletions
+11 -1
View File
@@ -69,6 +69,13 @@ final class AddMetricCommand
*/ */
public $default_view; public $default_view;
/**
* The threshold to buffer the metric points in.
*
* @var int
*/
public $threshold;
/** /**
* The validation rules. * The validation rules.
* *
@@ -84,6 +91,7 @@ final class AddMetricCommand
'display_chart' => 'int', 'display_chart' => 'int',
'places' => 'int|between:0,4', 'places' => 'int|between:0,4',
'default_view' => 'int|between:0,3', 'default_view' => 'int|between:0,3',
'threshold' => 'numeric|between:0,10',
]; ];
/** /**
@@ -97,10 +105,11 @@ final class AddMetricCommand
* @param int $display_chart * @param int $display_chart
* @param int $places * @param int $places
* @param int $default_view * @param int $default_view
* @param int $threshold
* *
* @return void * @return void
*/ */
public function __construct($name, $suffix, $description, $default_value, $calc_type, $display_chart, $places, $default_view) public function __construct($name, $suffix, $description, $default_value, $calc_type, $display_chart, $places, $default_view, $threshold)
{ {
$this->name = $name; $this->name = $name;
$this->suffix = $suffix; $this->suffix = $suffix;
@@ -110,5 +119,6 @@ final class AddMetricCommand
$this->display_chart = $display_chart; $this->display_chart = $display_chart;
$this->places = $places; $this->places = $places;
$this->default_view = $default_view; $this->default_view = $default_view;
$this->threshold = $threshold;
} }
} }
@@ -78,6 +78,13 @@ final class UpdateMetricCommand
*/ */
public $default_view; public $default_view;
/**
* The threshold to buffer the metric points in.
*
* @var int
*/
public $threshold;
/** /**
* The validation rules. * The validation rules.
* *
@@ -93,6 +100,7 @@ final class UpdateMetricCommand
'display_chart' => 'int', 'display_chart' => 'int',
'places' => 'numeric|between:0,4', 'places' => 'numeric|between:0,4',
'default_view' => 'numeric|between:0,4', 'default_view' => 'numeric|between:0,4',
'threshold' => 'numeric|between:0,10',
]; ];
/** /**
@@ -107,10 +115,11 @@ final class UpdateMetricCommand
* @param int $display_chart * @param int $display_chart
* @param int $places * @param int $places
* @param int $default_view * @param int $default_view
* @param int $threshold
* *
* @return void * @return void
*/ */
public function __construct(Metric $metric, $name, $suffix, $description, $default_value, $calc_type, $display_chart, $places, $default_view) public function __construct(Metric $metric, $name, $suffix, $description, $default_value, $calc_type, $display_chart, $places, $default_view, $threshold)
{ {
$this->metric = $metric; $this->metric = $metric;
$this->name = $name; $this->name = $name;
@@ -121,5 +130,6 @@ final class UpdateMetricCommand
$this->display_chart = $display_chart; $this->display_chart = $display_chart;
$this->places = $places; $this->places = $places;
$this->default_view = $default_view; $this->default_view = $default_view;
$this->threshold = $threshold;
} }
} }
@@ -14,6 +14,11 @@ namespace CachetHQ\Cachet\Bus\Commands\Metric;
use CachetHQ\Cachet\Models\Metric; use CachetHQ\Cachet\Models\Metric;
use CachetHQ\Cachet\Models\MetricPoint; use CachetHQ\Cachet\Models\MetricPoint;
/**
* This is the update metric point command.
*
* @author James Brooks <james@alt-three.com>
*/
final class UpdateMetricPointCommand final class UpdateMetricPointCommand
{ {
/** /**
@@ -33,7 +38,7 @@ final class UpdateMetricPointCommand
/** /**
* The metric point value. * The metric point value.
* *
* @var int * @var float
*/ */
public $value; public $value;
@@ -50,7 +55,7 @@ final class UpdateMetricPointCommand
* @var string[] * @var string[]
*/ */
public $rules = [ public $rules = [
'value' => 'int', 'value' => 'numeric',
'created_at' => 'string', 'created_at' => 'string',
]; ];
@@ -59,7 +64,7 @@ final class UpdateMetricPointCommand
* *
* @param \CachetHQ\Cachet\Models\MetricPoint $point * @param \CachetHQ\Cachet\Models\MetricPoint $point
* @param \CachetHQ\Cachet\Models\Metric $metric * @param \CachetHQ\Cachet\Models\Metric $metric
* @param int $value * @param float $value
* @param string $created_at * @param string $created_at
* *
* @return void * @return void
@@ -35,6 +35,7 @@ class AddMetricCommandHandler
'display_chart' => $command->display_chart, 'display_chart' => $command->display_chart,
'places' => $command->places, 'places' => $command->places,
'default_view' => $command->default_view, 'default_view' => $command->default_view,
'threshold' => $command->threshold,
]); ]);
event(new MetricWasAddedEvent($metric)); event(new MetricWasAddedEvent($metric));
@@ -15,6 +15,7 @@ use CachetHQ\Cachet\Bus\Commands\Metric\AddMetricPointCommand;
use CachetHQ\Cachet\Bus\Events\Metric\MetricPointWasAddedEvent; use CachetHQ\Cachet\Bus\Events\Metric\MetricPointWasAddedEvent;
use CachetHQ\Cachet\Dates\DateFactory; use CachetHQ\Cachet\Dates\DateFactory;
use CachetHQ\Cachet\Models\MetricPoint; use CachetHQ\Cachet\Models\MetricPoint;
use Carbon\Carbon;
class AddMetricPointCommandHandler class AddMetricPointCommandHandler
{ {
@@ -49,19 +50,35 @@ class AddMetricPointCommandHandler
$metric = $command->metric; $metric = $command->metric;
$createdAt = $command->created_at; $createdAt = $command->created_at;
$data = [ // Do we have an existing point with the same value?
'metric_id' => $metric->id, $point = $this->findOrCreatePoint($command);
'value' => $command->value,
];
if ($createdAt) { $point->increment('counter', 1);
$data['created_at'] = $this->dates->create('U', $createdAt)->format('Y-m-d H:i:s');
event(new MetricPointWasAddedEvent($point));
return $point;
}
protected function findOrCreatePoint(AddMetricPointCommand $command)
{
$buffer = Carbon::now()->subMinutes($command->metric->threshold);
$point = MetricPoint::where('metric_id', $command->metric->id)->where('value', $command->value)->where('created_at', '>=', $buffer)->first();
if ($point) {
return $point;
} }
$metricPoint = MetricPoint::create($data); $data = [
'metric_id' => $command->metric->id,
'value' => $command->value,
'counter' => 0,
];
event(new MetricPointWasAddedEvent($metricPoint)); if ($command->created_at) {
$data['created_at'] = $this->dates->create('U', $command->created_at)->format('Y-m-d H:i:s');
}
return $metricPoint; return MetricPoint::create($data);
} }
} }
@@ -53,6 +53,7 @@ class UpdateMetricCommandHandler
'display_chart' => $command->display_chart, 'display_chart' => $command->display_chart,
'places' => $command->places, 'places' => $command->places,
'default_view' => $command->default_view, 'default_view' => $command->default_view,
'threshold' => $command->threshold,
]; ];
return array_filter($params, function ($val) { return array_filter($params, function ($val) {
@@ -51,7 +51,7 @@ class UpdateMetricPointCommandHandler
$data = [ $data = [
'metric_id' => $metric->id, 'metric_id' => $metric->id,
'value' => $command->value, 'value' => (float) $command->value,
]; ];
if ($createdAt) { if ($createdAt) {
@@ -84,7 +84,8 @@ class MetricController extends AbstractApiController
Binput::get('calc_type', 0), Binput::get('calc_type', 0),
Binput::get('display_chart', true), Binput::get('display_chart', true),
Binput::get('places', 2), Binput::get('places', 2),
Binput::get('view', 1) Binput::get('view', 1),
Binput::get('threshold', 5)
)); ));
} catch (QueryException $e) { } catch (QueryException $e) {
throw new BadRequestHttpException(); throw new BadRequestHttpException();
@@ -112,7 +113,8 @@ class MetricController extends AbstractApiController
Binput::get('calc_type'), Binput::get('calc_type'),
Binput::get('display_chart'), Binput::get('display_chart'),
Binput::get('places'), Binput::get('places'),
Binput::get('view') Binput::get('view'),
Binput::get('threshold', 5)
)); ));
} catch (QueryException $e) { } catch (QueryException $e) {
throw new BadRequestHttpException(); throw new BadRequestHttpException();
@@ -48,8 +48,8 @@ class MetricPointController extends AbstractApiController
$metricPoint = dispatch(new AddMetricPointCommand( $metricPoint = dispatch(new AddMetricPointCommand(
$metric, $metric,
Binput::get('value'), Binput::get('value'),
Binput::get('timestamp')) Binput::get('timestamp')
); ));
} catch (QueryException $e) { } catch (QueryException $e) {
throw new BadRequestHttpException(); throw new BadRequestHttpException();
} }
@@ -79,7 +79,8 @@ class MetricController extends Controller
$metricData['calc_type'], $metricData['calc_type'],
$metricData['display_chart'], $metricData['display_chart'],
$metricData['places'], $metricData['places'],
$metricData['default_view'] $metricData['default_view'],
$metricData['threshold']
)); ));
} catch (ValidationException $e) { } catch (ValidationException $e) {
return Redirect::route('dashboard.metrics.add') return Redirect::route('dashboard.metrics.add')
@@ -151,7 +152,8 @@ class MetricController extends Controller
Binput::get('calc_type', null, false), Binput::get('calc_type', null, false),
Binput::get('display_chart', null, false), Binput::get('display_chart', null, false),
Binput::get('places', null, false), Binput::get('places', null, false),
Binput::get('default_view', null, false) Binput::get('default_view', null, false),
Binput::get('threshold', null, false)
)); ));
} catch (ValidationException $e) { } catch (ValidationException $e) {
return Redirect::route('dashboard.metrics.edit', ['id' => $metric->id]) return Redirect::route('dashboard.metrics.edit', ['id' => $metric->id])
+4
View File
@@ -47,6 +47,7 @@ class Metric extends Model implements HasPresenter
'calc_type' => 0, 'calc_type' => 0,
'places' => 2, 'places' => 2,
'default_view' => 1, 'default_view' => 1,
'threshold' => 5,
]; ];
/** /**
@@ -61,6 +62,7 @@ class Metric extends Model implements HasPresenter
'calc_type' => 'int', 'calc_type' => 'int',
'places' => 'int', 'places' => 'int',
'default_view' => 'int', 'default_view' => 'int',
'threshold' => 'int',
]; ];
/** /**
@@ -77,6 +79,7 @@ class Metric extends Model implements HasPresenter
'calc_type', 'calc_type',
'places', 'places',
'default_view', 'default_view',
'threshold',
]; ];
/** /**
@@ -91,6 +94,7 @@ class Metric extends Model implements HasPresenter
'default_value' => 'numeric', 'default_value' => 'numeric',
'places' => 'numeric|between:0,4', 'places' => 'numeric|between:0,4',
'default_view' => 'numeric|between:0,3', 'default_view' => 'numeric|between:0,3',
'threshold' => 'numeric|between:0,10',
]; ];
/** /**
+37 -3
View File
@@ -20,6 +20,16 @@ class MetricPoint extends Model implements HasPresenter
{ {
use ValidatingTrait; use ValidatingTrait;
/**
* The model's attributes.
*
* @var string[]
*/
protected $attributes = [
'value' => 0,
'counter' => 1,
];
/** /**
* The attributes that should be casted to native types. * The attributes that should be casted to native types.
* *
@@ -27,7 +37,8 @@ class MetricPoint extends Model implements HasPresenter
*/ */
protected $casts = [ protected $casts = [
'metric_id' => 'int', 'metric_id' => 'int',
'value' => 'int', 'value' => 'float',
'counter' => 'int',
]; ];
/** /**
@@ -35,7 +46,12 @@ class MetricPoint extends Model implements HasPresenter
* *
* @var string[] * @var string[]
*/ */
protected $fillable = ['metric_id', 'value', 'created_at']; protected $fillable = [
'metric_id',
'value',
'counter',
'created_at',
];
/** /**
* The validation rules. * The validation rules.
@@ -53,7 +69,25 @@ class MetricPoint extends Model implements HasPresenter
*/ */
public function metric() public function metric()
{ {
return $this->belongsTo(Metric::class, 'id', 'metric_id'); return $this->belongsTo(Metric::class);
}
/**
* Override the value attribute.
*
* @param mixed $value
*
* @return float
*/
public function getActiveValueAttribute($value)
{
if ($this->metric->calc_type === Metric::CALC_SUM) {
return round((float) $value * $this->counter, $this->metric->places);
} elseif ($this->metric->calc_type === Metric::CALC_AVG) {
return round((float) $value * $this->counter, $this->metric->places);
}
return round((float) $value, $this->metric->places);
} }
/** /**
+13 -2
View File
@@ -19,6 +19,16 @@ class MetricPointPresenter extends BasePresenter implements Arrayable
{ {
use TimestampsTrait; use TimestampsTrait;
/**
* Show the actual calculated value; as per (value * counter).
*
* @return int
*/
public function calculated_value()
{
return $this->wrappedObject->value * $this->wrappedObject->counter;
}
/** /**
* Convert the presenter instance to an array. * Convert the presenter instance to an array.
* *
@@ -27,8 +37,9 @@ class MetricPointPresenter extends BasePresenter implements Arrayable
public function toArray() public function toArray()
{ {
return array_merge($this->wrappedObject->toArray(), [ return array_merge($this->wrappedObject->toArray(), [
'created_at' => $this->created_at(), 'created_at' => $this->created_at(),
'updated_at' => $this->updated_at(), 'updated_at' => $this->updated_at(),
'calculated_value' => $this->calculated_value(),
]); ]);
} }
} }
+1 -1
View File
@@ -105,7 +105,7 @@ class MetricRepository
$points = []; $points = [];
$pointKey = $dateTime->format('jS M'); $pointKey = $dateTime->format('D jS M');
for ($i = 0; $i <= 7; $i++) { for ($i = 0; $i <= 7; $i++) {
$points[$pointKey] = $this->repository->getPointsForDayInWeek($metric, $i); $points[$pointKey] = $this->repository->getPointsForDayInWeek($metric, $i);
+39 -19
View File
@@ -32,14 +32,21 @@ class MySqlRepository implements MetricInterface
$dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H'))->sub(new DateInterval('PT'.$minute.'M')); $dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H'))->sub(new DateInterval('PT'.$minute.'M'));
$timeInterval = $dateTime->format('YmdHi'); $timeInterval = $dateTime->format('YmdHi');
$points = $metric->points()
->whereRaw('DATE_FORMAT(created_at, "%Y%m%d%H%i") = '.$timeInterval)
->groupBy(DB::raw('HOUR(created_at), MINUTE(created_at)'));
if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) { if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) {
$value = $points->sum('value'); $queryType = 'SUM(mp.`value` * mp.`counter`) AS `value`';
} elseif ($metric->calc_type == Metric::CALC_AVG) { } elseif ($metric->calc_type == Metric::CALC_AVG) {
$value = $points->avg('value'); $queryType = 'AVG(mp.`value` * mp.`counter`) AS `value`';
}
$value = 0;
$points = DB::select("SELECT {$queryType} FROM metrics m INNER JOIN metric_points mp ON m.id = mp.metric_id WHERE m.id = :metricId AND DATE_FORMAT(mp.`created_at`, '%Y%m%d%H%i') = :timeInterval GROUP BY HOUR(mp.`created_at`), MINUTE(mp.`created_at`)", [
'metricId' => $metric->id,
'timeInterval' => $timeInterval,
]);
if (isset($points[0]) && !($value = $points[0]->value)) {
$value = 0;
} }
if ($value === 0 && $metric->default_value != $value) { if ($value === 0 && $metric->default_value != $value) {
@@ -62,14 +69,21 @@ class MySqlRepository implements MetricInterface
$dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H')); $dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H'));
$hourInterval = $dateTime->format('YmdH'); $hourInterval = $dateTime->format('YmdH');
$points = $metric->points()
->whereRaw('DATE_FORMAT(created_at, "%Y%m%d%H") = '.$hourInterval)
->groupBy(DB::raw('HOUR(created_at)'));
if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) { if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) {
$value = $points->sum('value'); $queryType = 'SUM(mp.`value` * mp.`counter`) AS `value`';
} elseif ($metric->calc_type == Metric::CALC_AVG) { } elseif ($metric->calc_type == Metric::CALC_AVG) {
$value = $points->avg('value'); $queryType = 'AVG(mp.`value` * mp.`counter`) AS `value`';
}
$value = 0;
$points = DB::select("SELECT {$queryType} FROM metrics m INNER JOIN metric_points mp ON m.id = mp.metric_id WHERE m.id = :metricId AND DATE_FORMAT(mp.`created_at`, '%Y%m%d%H') = :hourInterval GROUP BY HOUR(mp.`created_at`)", [
'metricId' => $metric->id,
'hourInterval' => $hourInterval,
]);
if (isset($points[0]) && !($value = $points[0]->value)) {
$value = 0;
} }
if ($value === 0 && $metric->default_value != $value) { if ($value === 0 && $metric->default_value != $value) {
@@ -90,15 +104,21 @@ class MySqlRepository implements MetricInterface
{ {
$dateTime = (new Date())->sub(new DateInterval('P'.$day.'D')); $dateTime = (new Date())->sub(new DateInterval('P'.$day.'D'));
$points = $metric->points()
->whereRaw('created_at BETWEEN DATE_SUB(created_at, INTERVAL 1 WEEK) AND NOW()')
->whereRaw('DATE_FORMAT(created_at, "%Y%m%d") = '.$dateTime->format('Ymd'))
->groupBy(DB::raw('DATE_FORMAT(created_at, "%Y%m%d")'));
if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) { if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) {
$value = $points->sum('value'); $queryType = 'SUM(mp.`value` * mp.`counter`) AS `value`';
} elseif ($metric->calc_type == Metric::CALC_AVG) { } elseif ($metric->calc_type == Metric::CALC_AVG) {
$value = $points->avg('value'); $queryType = 'AVG(mp.`value` * mp.`counter`) AS `value`';
}
$value = 0;
$points = DB::select("SELECT {$queryType} FROM metrics m INNER JOIN metric_points mp ON m.id = mp.metric_id WHERE m.id = :metricId AND mp.`created_at` BETWEEN DATE_SUB(mp.`created_at`, INTERVAL 1 WEEK) AND DATE_ADD(NOW(), INTERVAL 1 DAY) AND DATE_FORMAT(mp.`created_at`, '%Y%m%d') = :timeInterval GROUP BY DATE_FORMAT(mp.`created_at`, '%Y%m%d')", [
'metricId' => $metric->id,
'timeInterval' => $dateTime->format('Ymd'),
]);
if (isset($points[0]) && !($value = $points[0]->value)) {
$value = 0;
} }
if ($value === 0 && $metric->default_value != $value) { if ($value === 0 && $metric->default_value != $value) {
+28 -27
View File
@@ -30,26 +30,24 @@ class PgSqlRepository implements MetricInterface
public function getPointsLastHour(Metric $metric, $hour, $minute) public function getPointsLastHour(Metric $metric, $hour, $minute)
{ {
$dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H'))->sub(new DateInterval('PT'.$minute.'M')); $dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H'))->sub(new DateInterval('PT'.$minute.'M'));
$hourInterval = $dateTime->format('YmdHi');
// Default metrics calculations. // Default metrics calculations.
if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) { if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) {
$queryType = 'sum(metric_points.value)'; $queryType = 'sum(metric_points.value * metric_points.counter)';
} elseif ($metric->calc_type == Metric::CALC_AVG) { } elseif ($metric->calc_type == Metric::CALC_AVG) {
$queryType = 'avg(metric_points.value)'; $queryType = 'avg(metric_points.value * metric_points.counter)';
} else { } else {
$queryType = 'sum(metric_points.value)'; $queryType = 'sum(metric_points.value * metric_points.counter)';
} }
$query = DB::select("select {$queryType} as aggregate FROM metrics JOIN metric_points ON metric_points.metric_id = metrics.id WHERE metrics.id = :metric_id AND to_char(metric_points.created_at, 'YYYYMMDDHH24MI') = :timestamp GROUP BY to_char(metric_points.created_at, 'HHMI')", [ $value = 0;
'metric_id' => $metric->id, $query = DB::select("select {$queryType} as value FROM metrics JOIN metric_points ON metric_points.metric_id = metrics.id WHERE metrics.id = :metricId AND to_char(metric_points.created_at, 'YYYYMMDDHH24MI') = :timeInterval GROUP BY to_char(metric_points.created_at, 'HHMI')", [
'timestamp' => $hourInterval, 'metricId' => $metric->id,
'timeInterval' => $dateTime->format('YmdHi'),
]); ]);
if (isset($query[0])) { if (isset($query[0])) {
$value = $query[0]->aggregate; $value = $query[0]->value;
} else {
$value = 0;
} }
if ($value === 0 && $metric->default_value != $value) { if ($value === 0 && $metric->default_value != $value) {
@@ -70,26 +68,24 @@ class PgSqlRepository implements MetricInterface
public function getPointsByHour(Metric $metric, $hour) public function getPointsByHour(Metric $metric, $hour)
{ {
$dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H')); $dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H'));
$hourInterval = $dateTime->format('YmdH');
// Default metrics calculations. // Default metrics calculations.
if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) { if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) {
$queryType = 'sum(metric_points.value)'; $queryType = 'sum(metric_points.value * metric_points.counter)';
} elseif ($metric->calc_type == Metric::CALC_AVG) { } elseif ($metric->calc_type == Metric::CALC_AVG) {
$queryType = 'avg(metric_points.value)'; $queryType = 'avg(metric_points.value * metric_points.counter)';
} else { } else {
$queryType = 'sum(metric_points.value)'; $queryType = 'sum(metric_points.value * metric_points.counter)';
} }
$query = DB::select("select {$queryType} as aggregate FROM metrics JOIN metric_points ON metric_points.metric_id = metrics.id WHERE metric_points.metric_id = :metric_id AND to_char(metric_points.created_at, 'YYYYMMDDHH24') = :timestamp GROUP BY to_char(metric_points.created_at, 'H')", [ $value = 0;
'metric_id' => $metric->id, $query = DB::select("select {$queryType} as value FROM metrics JOIN metric_points ON metric_points.metric_id = metrics.id WHERE metric_points.metric_id = :metricId AND to_char(metric_points.created_at, 'YYYYMMDDHH24') = :timeInterval GROUP BY to_char(metric_points.created_at, 'H')", [
'timestamp' => $hourInterval, 'metricId' => $metric->id,
'timeInterval' => $dateTime->format('YmdH'),
]); ]);
if (isset($query[0])) { if (isset($query[0])) {
$value = $query[0]->aggregate; $value = $query[0]->value;
} else {
$value = 0;
} }
if ($value === 0 && $metric->default_value != $value) { if ($value === 0 && $metric->default_value != $value) {
@@ -110,15 +106,20 @@ class PgSqlRepository implements MetricInterface
{ {
$dateTime = (new Date())->sub(new DateInterval('P'.$day.'D')); $dateTime = (new Date())->sub(new DateInterval('P'.$day.'D'));
$points = $metric->points()
->whereRaw('created_at BETWEEN (created_at - interval \'1 week\') AND now()')
->whereRaw('to_char(created_at, \'YYYYMMDD\') = \''.$dateTime->format('Ymd').'\'')
->groupBy(DB::raw('to_char(created_at, \'YYYYMMDD\')'));
if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) { if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) {
$value = $points->sum('value'); $queryType = 'sum(mp.`value` * mp.`counter`) AS `value`';
} elseif ($metric->calc_type == Metric::CALC_AVG) { } elseif ($metric->calc_type == Metric::CALC_AVG) {
$value = $points->avg('value'); $queryType = 'avg(mp.`value` * mp.`counter`) AS `value`';
}
$value = 0;
$points = DB::select("SELECT {$queryType} FROM metrics m INNER JOIN metric_points mp ON m.id = mp.metric_id WHERE m.id = :metricId AND mp.`created_at` BETWEEN (mp.`created_at` - interval '1 week') AND (now() + interval 1 day) AND to_char(mp.`created_at`, 'YYYYMMDD') = :timeInterval GROUP BY to_char(mp.`created_at`, 'YYYYMMDD')", [
'metricId' => $metric->id,
'timeInterval' => $dateTime->format('Ymd'),
]);
if (isset($points[0]) && !($value = $points[0]->value)) {
$value = 0;
} }
if ($value === 0 && $metric->default_value != $value) { if ($value === 0 && $metric->default_value != $value) {
+45 -21
View File
@@ -30,16 +30,24 @@ class SqliteRepository implements MetricInterface
public function getPointsLastHour(Metric $metric, $hour, $minute) public function getPointsLastHour(Metric $metric, $hour, $minute)
{ {
$dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H'))->sub(new DateInterval('PT'.$minute.'M')); $dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H'))->sub(new DateInterval('PT'.$minute.'M'));
$hourInterval = $dateTime->format('YmdHi');
$points = $metric->points()
->whereRaw('strftime("%Y%m%d%H%M", created_at) = "'.$hourInterval.'"')
->groupBy(DB::raw('strftime("%H%M", created_at)'));
// Default metrics calculations.
if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) { if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) {
$value = $points->sum('value'); $queryType = 'sum(metric_points.value * metric_points.counter)';
} elseif ($metric->calc_type == Metric::CALC_AVG) { } elseif ($metric->calc_type == Metric::CALC_AVG) {
$value = $points->avg('value'); $queryType = 'avg(metric_points.value * metric_points.counter)';
} else {
$queryType = 'sum(metric_points.value * metric_points.counter)';
}
$value = 0;
$query = DB::select("select {$queryType} as value FROM metrics JOIN metric_points ON metric_points.metric_id = metrics.id WHERE metrics.id = :metricId AND strftime('%Y%m%d%H%M', metric_points.created_at) = :timeInterval GROUP BY strftime('%H%M', metric_points.created_at)", [
'metricId' => $metric->id,
'timeInterval' => $dateTime->format('YmdHi'),
]);
if (isset($query[0])) {
$value = $query[0]->value;
} }
if ($value === 0 && $metric->default_value != $value) { if ($value === 0 && $metric->default_value != $value) {
@@ -60,16 +68,24 @@ class SqliteRepository implements MetricInterface
public function getPointsByHour(Metric $metric, $hour) public function getPointsByHour(Metric $metric, $hour)
{ {
$dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H')); $dateTime = (new Date())->sub(new DateInterval('PT'.$hour.'H'));
$hourInterval = $dateTime->format('YmdH');
$points = $metric->points()
->whereRaw('strftime("%Y%m%d%H", created_at) = "'.$hourInterval.'"')
->groupBy(DB::raw('strftime("%H", created_at)'));
// Default metrics calculations.
if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) { if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) {
$value = $points->sum('value'); $queryType = 'sum(metric_points.value * metric_points.counter)';
} elseif ($metric->calc_type == Metric::CALC_AVG) { } elseif ($metric->calc_type == Metric::CALC_AVG) {
$value = $points->avg('value'); $queryType = 'avg(metric_points.value * metric_points.counter)';
} else {
$queryType = 'sum(metric_points.value * metric_points.counter)';
}
$value = 0;
$query = DB::select("select {$queryType} as value FROM metrics JOIN metric_points ON metric_points.metric_id = metrics.id WHERE metrics.id = :metricId AND strftime('%Y%m%d%H', metric_points.created_at) = :timeInterval GROUP BY strftime('%H', metric_points.created_at)", [
'metricId' => $metric->id,
'timeInterval' => $dateTime->format('YmdH'),
]);
if (isset($query[0])) {
$value = $query[0]->value;
} }
if ($value === 0 && $metric->default_value != $value) { if ($value === 0 && $metric->default_value != $value) {
@@ -90,15 +106,23 @@ class SqliteRepository implements MetricInterface
{ {
$dateTime = (new Date())->sub(new DateInterval('P'.$day.'D')); $dateTime = (new Date())->sub(new DateInterval('P'.$day.'D'));
$points = $metric->points() // Default metrics calculations.
->whereRaw('created_at > date("now", "-7 day")')
->whereRaw('strftime("%Y%m%d", created_at) = "'.$dateTime->format('Ymd').'"')
->groupBy(DB::raw('strftime("%Y%m%d", created_at)'));
if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) { if (!isset($metric->calc_type) || $metric->calc_type == Metric::CALC_SUM) {
$value = $points->sum('value'); $queryType = 'sum(metric_points.value * metric_points.counter)';
} elseif ($metric->calc_type == Metric::CALC_AVG) { } elseif ($metric->calc_type == Metric::CALC_AVG) {
$value = $points->avg('value'); $queryType = 'avg(metric_points.value * metric_points.counter)';
} else {
$queryType = 'sum(metric_points.value * metric_points.counter)';
}
$value = 0;
$query = DB::select("select {$queryType} as value FROM metrics JOIN metric_points ON metric_points.metric_id = metrics.id WHERE metrics.id = :metricId AND metric_points.created_at > date('now', '-7 day') AND strftime('%Y%m%d', metric_points.created_at) = :timeInterval GROUP BY strftime('%Y%m%d', metric_points.created_at)", [
'metricId' => $metric->id,
'timeInterval' => $dateTime->format('Ymd'),
]);
if (isset($query[0])) {
$value = $query[0]->value;
} }
if ($value === 0 && $metric->default_value != $value) { if ($value === 0 && $metric->default_value != $value) {
+3
View File
@@ -61,8 +61,10 @@ $factory->define(Metric::class, function ($faker) {
'suffix' => $faker->word(), 'suffix' => $faker->word(),
'description' => $faker->paragraph(), 'description' => $faker->paragraph(),
'default_value' => 1, 'default_value' => 1,
'places' => 2,
'calc_type' => $faker->boolean(), 'calc_type' => $faker->boolean(),
'display_chart' => $faker->boolean(), 'display_chart' => $faker->boolean(),
'threshold' => 5,
]; ];
}); });
@@ -70,6 +72,7 @@ $factory->define(MetricPoint::class, function ($faker) {
return [ return [
'metric_id' => factory(Metric::class)->create()->id, 'metric_id' => factory(Metric::class)->create()->id,
'value' => random_int(1, 100), 'value' => random_int(1, 100),
'counter' => 1,
]; ];
}); });
@@ -0,0 +1,49 @@
<?php
/*
* This file is part of Cachet.
*
* (c) Alt Three Services Limited
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AlterTableMetricPointsAddCounterColumn extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('metrics', function (Blueprint $table) {
$table->integer('threshold')->unsigned()->default(5)->after('default_view');
});
Schema::table('metric_points', function (Blueprint $table) {
$table->integer('counter')->unsigned()->default(1)->after('value');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('metrics', function (Blueprint $table) {
$table->dropColumn('threshold');
});
Schema::table('metric_points', function (Blueprint $table) {
$table->dropColumn('counter');
});
}
}
+1
View File
@@ -89,6 +89,7 @@ return [
'type_avg' => 'Average', 'type_avg' => 'Average',
'places' => 'Decimal places', 'places' => 'Decimal places',
'default_view' => 'Default view', 'default_view' => 'Default view',
'threshold' => 'How many minutes of threshold between metric points?',
'points' => [ 'points' => [
'value' => 'Value', 'value' => 'Value',
@@ -55,6 +55,10 @@
<label for="metric-places">{{ trans('forms.metrics.places') }}</label> <label for="metric-places">{{ trans('forms.metrics.places') }}</label>
<input type="number" min="0" max="4" class="form-control" name="metric[places]" id="metric-places" required value="{{ Binput::old('metric.places') }}"> <input type="number" min="0" max="4" class="form-control" name="metric[places]" id="metric-places" required value="{{ Binput::old('metric.places') }}">
</div> </div>
<div class="form-group">
<label for="metric-places">{{ trans('forms.metrics.threshold') }}</label>
<input type="number" min="0" max="100" class="form-control" name="metric[threshold]" id="metric-threshold" required value="{{ Binput::old('metric.threshold') }}">
</div>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="hidden" value="0" name="metric[display_chart]"> <input type="hidden" value="0" name="metric[display_chart]">
@@ -55,6 +55,10 @@
<label for="metric-places">{{ trans('forms.metrics.places') }}</label> <label for="metric-places">{{ trans('forms.metrics.places') }}</label>
<input type="number" min="0" max="4" class="form-control" name="places" id="metric-places" required value="{{ $metric->places }}"> <input type="number" min="0" max="4" class="form-control" name="places" id="metric-places" required value="{{ $metric->places }}">
</div> </div>
<div class="form-group">
<label for="metric-places">{{ trans('forms.metrics.threshold') }}</label>
<input type="number" min="0" max="100" class="form-control" name="threshold" id="metric-threshold" required value="{{ $metric->threshold }}">
</div>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="hidden" value="0" name="display_chart"> <input type="hidden" value="0" name="display_chart">
+18
View File
@@ -29,9 +29,12 @@ class MetricPointTest extends AbstractApiTestCase
]); ]);
$this->get("/api/v1/metrics/{$metric->id}/points"); $this->get("/api/v1/metrics/{$metric->id}/points");
$this->seeJson(['id' => $metricPoint[0]->id]); $this->seeJson(['id' => $metricPoint[0]->id]);
$this->seeJson(['id' => $metricPoint[1]->id]); $this->seeJson(['id' => $metricPoint[1]->id]);
$this->seeJson(['id' => $metricPoint[2]->id]); $this->seeJson(['id' => $metricPoint[2]->id]);
$this->assertResponseOk();
} }
public function testPostMetricPointUnauthorized() public function testPostMetricPointUnauthorized()
@@ -55,7 +58,10 @@ class MetricPointTest extends AbstractApiTestCase
]); ]);
$this->post("/api/v1/metrics/{$metric->id}/points", $metricPoint->toArray()); $this->post("/api/v1/metrics/{$metric->id}/points", $metricPoint->toArray());
$this->seeJson(['value' => $metricPoint->value]); $this->seeJson(['value' => $metricPoint->value]);
$this->assertResponseOk();
} }
public function testPostMetricPointTimestamp() public function testPostMetricPointTimestamp()
@@ -72,7 +78,10 @@ class MetricPointTest extends AbstractApiTestCase
$postData['timestamp'] = $timestamp; $postData['timestamp'] = $timestamp;
$this->post("/api/v1/metrics/{$metric->id}/points", $postData); $this->post("/api/v1/metrics/{$metric->id}/points", $postData);
$this->seeJson(['value' => $metricPoint->value, 'created_at' => $datetime]); $this->seeJson(['value' => $metricPoint->value, 'created_at' => $datetime]);
$this->assertResponseOk();
} }
public function testPostMetricPointTimestampTimezone() public function testPostMetricPointTimestampTimezone()
@@ -96,7 +105,10 @@ class MetricPointTest extends AbstractApiTestCase
$postData['timestamp'] = $datetime->timestamp; $postData['timestamp'] = $datetime->timestamp;
$this->post("/api/v1/metrics/{$metric->id}/points", $postData, ['Time-Zone' => $timezone]); $this->post("/api/v1/metrics/{$metric->id}/points", $postData, ['Time-Zone' => $timezone]);
$this->seeJson(['value' => $metricPoint->value, 'created_at' => $datetime->toDateTimeString()]); $this->seeJson(['value' => $metricPoint->value, 'created_at' => $datetime->toDateTimeString()]);
$this->assertResponseOk();
} }
public function testPutMetricPoint() public function testPutMetricPoint()
@@ -106,10 +118,14 @@ class MetricPointTest extends AbstractApiTestCase
$metricPoint = factory('CachetHQ\Cachet\Models\MetricPoint')->create([ $metricPoint = factory('CachetHQ\Cachet\Models\MetricPoint')->create([
'metric_id' => $metric->id, 'metric_id' => $metric->id,
]); ]);
$this->put("/api/v1/metrics/{$metric->id}/points/{$metricPoint->id}", [ $this->put("/api/v1/metrics/{$metric->id}/points/{$metricPoint->id}", [
'value' => 999, 'value' => 999,
]); ]);
$this->seeJson(['value' => 999]); $this->seeJson(['value' => 999]);
$this->assertResponseOk();
} }
public function testDeleteMetricPoint() public function testDeleteMetricPoint()
@@ -119,7 +135,9 @@ class MetricPointTest extends AbstractApiTestCase
$metricPoint = factory('CachetHQ\Cachet\Models\MetricPoint')->create([ $metricPoint = factory('CachetHQ\Cachet\Models\MetricPoint')->create([
'metric_id' => $metric->id, 'metric_id' => $metric->id,
]); ]);
$this->delete("/api/v1/metrics/{$metric->id}/points/{$metricPoint->id}"); $this->delete("/api/v1/metrics/{$metric->id}/points/{$metricPoint->id}");
$this->assertResponseStatus(204); $this->assertResponseStatus(204);
} }
} }
@@ -29,14 +29,15 @@ class AddMetricCommandTest extends AbstractTestCase
protected function getObjectAndParams() protected function getObjectAndParams()
{ {
$params = [ $params = [
'name' => 'Coffee', 'name' => 'Coffee',
'suffix' => 'cups', 'suffix' => 'cups',
'description' => 'Cups of coffee consumed', 'description' => 'Cups of coffee consumed',
'default_value' => 0, 'default_value' => 0,
'calc_type' => 0, 'calc_type' => 0,
'display_chart' => 1, 'display_chart' => 1,
'places' => 0, 'places' => 0,
'default_view' => 0, 'default_view' => 0,
'threshold' => 0,
]; ];
$object = new AddMetricCommand( $object = new AddMetricCommand(
@@ -47,7 +48,8 @@ class AddMetricCommandTest extends AbstractTestCase
$params['calc_type'], $params['calc_type'],
$params['display_chart'], $params['display_chart'],
$params['places'], $params['places'],
$params['default_view'] $params['default_view'],
$params['threshold']
); );
return compact('params', 'object'); return compact('params', 'object');
@@ -39,6 +39,7 @@ class UpdateMetricCommandTest extends AbstractTestCase
'display_chart' => 1, 'display_chart' => 1,
'places' => 0, 'places' => 0,
'default_view' => 0, 'default_view' => 0,
'threshold' => 0,
]; ];
$object = new UpdateMetricCommand( $object = new UpdateMetricCommand(
@@ -50,7 +51,8 @@ class UpdateMetricCommandTest extends AbstractTestCase
$params['calc_type'], $params['calc_type'],
$params['display_chart'], $params['display_chart'],
$params['places'], $params['places'],
$params['default_view'] $params['default_view'],
$params['threshold']
); );
return compact('params', 'object'); return compact('params', 'object');