Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heartbeats #21

Merged
merged 14 commits into from
May 7, 2015
60 changes: 60 additions & 0 deletions app/Console/Commands/CheckHeartbeats.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php namespace App\Console\Commands;

use Queue;
use Carbon\Carbon;
use App\Heartbeat;
use App\Commands\Notify;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;

/**
* Checks that any expected heartbeats have checked-in
*/
class CheckHeartbeats extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'heartbeat:check';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Checks that any expected heartbeats have checked-in';

/**
* Checks that heartbeats happened as expected
*
* @return mixed
*/
public function fire()
{
$heartbeats = Heartbeat::all();

foreach ($heartbeats as $heartbeat) {
$last_heard_from = $heartbeat->last_activity;
if ($heartbeat->status === Heartbeat::UNTESTED) {
$last_heard_from = $heartbeat->created_at;
}

$missed = $heartbeat->missed + 1;

$next_time = $last_heard_from->addMinutes($heartbeat->interval * $missed);

if (Carbon::now()->gt($next_time)) {
$heartbeat->status = Heartbeat::MISSING;
$heartbeat->missed = $missed;
$heartbeat->save();

foreach ($heartbeat->project->notifications as $notification) {
Queue::pushOn('notify', new Notify($notification, $heartbeat->notificationPayload()));
}
}
}
}
}
6 changes: 4 additions & 2 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [

'App\Console\Commands\CheckHeartbeats'
];

/**
Expand All @@ -26,6 +26,8 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
//
$schedule->command('heartbeat:check')
->everyFiveMinutes()
->withoutOverlapping();
}
}
159 changes: 159 additions & 0 deletions app/Heartbeat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php namespace App;

use Lang;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;

/**
* Heartbeat model
*/
class Heartbeat extends Model
{
use SoftDeletes;

const OK = 0;
const UNTESTED = 1;
const MISSING = 2;

/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['project_id', 'created_at', 'updated_at', 'deleted_at', 'pivot'];

/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['name', 'interval', 'project_id'];

/**
* The fields which should be tried as Carbon instances
*
* @var array
*/
protected $dates = ['last_activity'];

/**
* Additional attributes to include in the JSON representation
*
* @var array
*/
protected $appends = ['callback_url'];

/**
* The attributes that should be casted to native types.
*
* @var array
*/
protected $casts = [
'status' => 'integer',
'deploy_code' => 'boolean'
];

/**
* Belongs to relationship
*
* @return Project
*/
public function project()
{
return $this->belongsTo('App\Project');
}

/**
* Override the boot method to bind model event listeners
*
* @return void
*/
public static function boot()
{
parent::boot();

// When first creating the model generate a webhook hash
static::creating(function ($model) {
if (!array_key_exists('hash', $model->attributes)) {
$model->generateHash();
}
});
}

/**
* Generates a hash for use in the webhook URL
*
* @return void
*/
public function generateHash()
{
$this->attributes['hash'] = Str::quickRandom(30);
}

/**
* Define a mutator for the callback URL
*
* @return string
* @todo Shouldn't this be a presenter?
*/
public function getCallbackUrlAttribute()
{
return route('heartbeat', $this->hash);
}

/**
* Updates the last_activity timestamp
*
* @return boolean
*/
public function pinged()
{
$this->status = self::OK;
$this->missed = 0;
$this->last_activity = $this->freshTimestamp();

return $this->save();
}

/**
* Generates a slack payload for the heartbeat failuyre
*
* @return array
*/
public function notificationPayload()
{
$message = Lang::get('heartbeats.message', [ 'job' => $this->name ]);

if (is_null($this->last_activity)) {
$heard_from = Lang::get('app.never');
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHPCS: Expected 1 space after closing brace; newline found

else {
$heard_from = $this->last_activity->diffForHumans();
}

$payload = [
'attachments' => [
[
'fallback' => $message,
'text' => $message,
'color' => 'danger',
'fields' => [
[
'title' => Lang::get('notifications.project'),
'value' => sprintf('<%s|%s>', url('project', $this->project_id), $this->project->name),
'short' => true
], [
'title' => Lang::get('heartbeats.last_check_in'),
'value' => $heard_from,
'short' => true
]
]
]
]
];

return $payload;
}
}
74 changes: 74 additions & 0 deletions app/Http/Controllers/HeartbeatController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php namespace App\Http\Controllers;

use App\Heartbeat;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreHeartbeatRequest;

/**
* Controller for managing notifications
*/
class HeartbeatController extends Controller
{
/**
* Handles the callback URL for the heartbeat
*
* @param string $hash The webhook hash
* @return Response
*/
public function ping($hash)
{
$heartbeat = Heartbeat::where('hash', $hash)
->firstOrFail();

$heartbeat->pinged();

return 'OK';
}

/**
* Store a newly created heartbeat in storage.
*
* @param StoreHeartbeatRequest $request
* @return Response
*/
public function store(StoreHeartbeatRequest $request)
{
return Heartbeat::create($request->only(
'name',
'interval',
'project_id'
));
}

/**
* Update the specified heartbeat in storage.
*
* @param Heartbeat $heartbeat
* @param StoreHeartbeatRequest $request
* @return Response
*/
public function update(Heartbeat $heartbeat, StoreHeartbeatRequest $request)
{
$heartbeat->update($request->only(
'name',
'interval'
));

return $heartbeat;
}

/**
* Remove the specified heartbeat from storage.
*
* @param Heartbeat $heartbeat
* @return Response
*/
public function destroy(Heartbeat $heartbeat)
{
$heartbeat->delete();

return [
'success' => true
];
}
}
3 changes: 1 addition & 2 deletions app/Http/Controllers/NotificationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ public function update(Notification $notification, StoreNotificationRequest $req
$notification->update($request->only(
'name',
'channel',
'webhook',
'project_id'
'webhook'
));

return $notification;
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public function show(Project $project, DeploymentRepositoryInterface $deployment
'project' => $project,
'servers' => $project->servers,
'notifications' => $project->notifications,
'heartbeats' => $project->heartbeats,
'optional' => $optional
]);
}
Expand Down
22 changes: 22 additions & 0 deletions app/Http/Requests/StoreHeartbeatRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php namespace App\Http\Requests;

use App\Http\Requests\Request;

/**
* Request for validating heartbeats
*/
class StoreHeartbeatRequest extends Request
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|max:255',
'project_id' => 'required|integer|exists:projects,id'
];
}
}
9 changes: 9 additions & 0 deletions app/Http/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
'only' => ['show', 'store', 'update', 'destroy']
]);

Route::resource('heartbeats', 'HeartbeatController', [
'only' => ['store', 'update', 'destroy']
]);

Route::resource('notifications', 'NotificationController', [
'only' => ['store', 'update', 'destroy']
]);
Expand Down Expand Up @@ -76,6 +80,11 @@
'uses' => 'WebhookController@webhook'
]);

Route::get('heartbeat/{hash}', [
'as' => 'heartbeat',
'uses' => 'HeartbeatController@ping'
]);

Route::controllers([
'auth' => 'Auth\AuthController',
'password' => 'Auth\PasswordController'
Expand Down
10 changes: 10 additions & 0 deletions app/Project.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ public function servers()
return $this->hasMany('App\Server')->orderBy('name');
}

/**
* Has many relationship
*
* @return Heartbeat
*/
public function heartbeats()
{
return $this->hasMany('App\Heartbeat')->orderBy('name');
}

/**
* Has many relationship
*
Expand Down
Loading