Sky Puppy's modular architecture allows you to create custom checkers for any type of service or system. This guide shows you how to build your own checkers and integrate them with Sky Puppy.
A Sky Puppy checker is a Node.js module that exports a function with specific methods. The checker is responsible for:
class MyCustomChecker {
constructor(config, service, settings) {
this.config = config;
this.service = service;
this.settings = settings;
}
async init() {
// Initialize connections, validate settings, etc.
}
async check() {
// Perform the health check
// Return: { code: number, message: string }
}
}
module.exports = function(config, service, settings) {
return new MyCustomChecker(config, service, settings);
};
Let's create a simple checker that monitors a file's existence:
const fs = require('fs').promises;
const path = require('path');
class FileChecker {
constructor(config, service, settings) {
this.config = config;
this.service = service;
this.settings = settings;
this.filePath = settings.file_path;
}
async init() {
if (!this.filePath) {
throw new Error('file_path is required for FileChecker');
}
// Validate that the file path is safe
if (path.isAbsolute(this.filePath) && !this.filePath.startsWith('/safe/directory')) {
throw new Error('File path must be within safe directory');
}
}
async check() {
try {
await fs.access(this.filePath);
return {
code: 200,
message: `File ${this.filePath} exists`
};
} catch (error) {
return {
code: 404,
message: `File ${this.filePath} not found: ${error.message}`
};
}
}
}
module.exports = function(config, service, settings) {
return new FileChecker(config, service, settings);
};
Here's a more complex checker that monitors a Redis database:
const Redis = require('ioredis');
class RedisChecker {
constructor(config, service, settings) {
this.config = config;
this.service = service;
this.settings = settings;
this.client = null;
this.timeout = settings.timeout || 5000;
}
async init() {
if (!this.settings.uri) {
throw new Error('Redis URI is required');
}
this.client = new Redis(this.settings.uri, {
lazyConnect: true,
commandTimeout: this.timeout,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3
});
// Test the connection
await this.client.ping();
}
async check() {
try {
const startTime = Date.now();
// Test basic operations
await this.client.ping();
await this.client.set('sky-puppy-test', 'ok', 'EX', 10);
const result = await this.client.get('sky-puppy-test');
const responseTime = Date.now() - startTime;
if (result === 'ok') {
return {
code: 200,
message: `Redis is responding (${responseTime}ms)`
};
} else {
return {
code: 500,
message: 'Redis test operation failed'
};
}
} catch (error) {
return {
code: 503,
message: `Redis connection failed: ${error.message}`
};
}
}
async close() {
if (this.client) {
await this.client.quit();
this.client = null;
}
}
}
module.exports = function(config, service, settings) {
return new RedisChecker(config, service, settings);
};
To use your custom checker, add it to your Sky Puppy configuration:
{
"checkers": {
"my-file-checker": {
"file_path": "/var/log/important.log"
},
"my-redis-checker": {
"uri": "redis://localhost:6379",
"timeout": 5000,
"code_messages": {
"200": "Redis is healthy",
"503": "Redis connection failed"
}
}
},
"services": {
"my-file": {
"interval": 60,
"checker": {
"name": "my-file-checker",
"settings": {
"file_path": "/var/log/important.log"
},
"code_messages": {
"200": "File exists and is accessible",
"404": "File not found"
}
}
},
"redis-db": {
"interval": 30,
"checker": {
"name": "my-redis-checker",
"settings": {
"uri": "redis://localhost:6379",
"timeout": 5000
},
"code_messages": {
"200": "Redis is responding normally",
"503": "Redis is unavailable"
}
}
}
}
}
You can customize the messages returned by your checker for different status codes using the code_messages
feature:
{
"services": {
"my-service": {
"interval": 30,
"checker": {
"name": "request",
"settings": {
"uri": "https://api.example.com/health"
},
"code_messages": {
"200": "Service is healthy and responding",
"500": "Service is experiencing internal errors",
"503": "Service is temporarily unavailable",
"404": "Health endpoint not found"
}
}
}
}
}
The code_messages
object maps HTTP status codes to custom messages. When your checker returns a specific status code, Sky Puppy will use the corresponding message instead of the default one.
code_messages
can be defined both globally in the checkers
section and locally in each service's checker configuration. Local settings override global ones.
Sky Puppy comes with a built-in HTTP request checker:
The request
checker is the default HTTP health checker:
{
"services": {
"web-service": {
"interval": 30,
"checker": {
"name": "request",
"settings": {
"uri": "https://api.example.com/health",
"method": "GET",
"timeout": 10,
"headers": {
"Authorization": "Bearer your-token"
},
"body": {
"check": "database"
},
"json": true
}
}
}
}
}
Option | Type | Default | Description |
---|---|---|---|
uri |
string | required | URL to check |
method |
string | GET | HTTP method |
timeout |
number | 60 | Timeout in seconds |
headers |
object | {} | HTTP headers |
body |
any | null | Request body |
json |
boolean | false | Send body as JSON |
To make your checker available to others, publish it as an npm package:
sky-puppy-checker-{service-name}
format
{
"name": "sky-puppy-checker-my-service",
"version": "1.0.0",
"description": "Sky Puppy checker for MyService",
"main": "index.js",
"keywords": ["sky-puppy", "checker", "health", "monitoring"],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"my-service-client": "^1.0.0"
}
}
const MyServiceChecker = require('./checker');
module.exports = MyServiceChecker;
npm publish
Always handle errors gracefully and provide meaningful error messages:
async check() {
try {
// Your check logic
return { code: 200, message: 'Service is up' };
} catch (error) {
// Log the full error for debugging
console.error('Checker error:', error);
// Return user-friendly message
return {
code: 503,
message: `Service unavailable: ${error.message}`
};
}
}
Properly manage connections and resources:
async init() {
this.connection = await createConnection(this.settings);
}
async close() {
if (this.connection) {
await this.connection.close();
this.connection = null;
}
}
Validate required settings in the init()
method:
async init() {
const required = ['host', 'port', 'database'];
for (const field of required) {
if (!this.settings[field]) {
throw new Error(`${field} is required for this checker`);
}
}
}
Create tests for your checker:
const assert = require('assert');
const MyChecker = require('./checker');
describe('MyChecker', () => {
let checker;
beforeEach(async () => {
checker = new MyChecker({}, {}, { uri: 'test-uri' });
await checker.init();
});
afterEach(async () => {
await checker.close();
});
it('should return healthy when service is up', async () => {
const result = await checker.check();
assert.strictEqual(result.code, 200);
});
it('should return down when service is unavailable', async () => {
// Mock service to be down
const result = await checker.check();
assert.strictEqual(result.code, 503);
});
});