# Quick Start
Kener has been tested from Node18. It should work with Node 16 and above. It uses [SvelteKit](https://kit.svelte.dev/) and [shadcn-svelte](https://www.shadcn-svelte.com/)
## Clone the repository
```shell
git clone https://github.com/rajnandan1/kener.git
cd kener
```
## Install Dependencies
```shell
npm install
```
## Configs
- Rename `config/site.example.yaml` -> `config/site.yaml`
- Rename `config/monitors.example.yaml` -> `config/monitors.yaml`
```shell
mv config/site.example.yaml config/site.yaml
mv config/monitors.example.yaml config/monitors.yaml
mkdir -p ./static/kener
```
## Start Kener Dev
```bash
npm run kener:dev
```
Kener Development Server would be running at PORT 5173. Go to [http://localhost:5173](http://localhost:5173)

## Concepts
Kener has two parts. One is a svelte app which you can find in the src folder and there are code for monitors which you would find in scripts folder. If you want to update the frontend application then you should modify the src folder.
## Folder structure
```shell
├── src (svelte frontend files)
├── static (things put here can be referenced directly example static/logo.png -> /logo.png)
├── scripts (nodejs server files)
├── prod.js(starts an express server, runs the scripts and serves the svelte site)
├── dev.js (starts the dev server)
```
## Environment Variable
#### PUBLIC_KENER_FOLDER (Required)
```shell
export PUBLIC_KENER_FOLDER=/path/to/a/directory
```
#### PORT
Defaults to 3000 if not specified
```shell
export PORT=4242
```
#### GH_TOKEN
A github token to read issues and create labels
```shell
export GH_TOKEN=your-github-token
```
#### API_TOKEN
To talk to kener apis you will need to set up a token. It uses Bearer Authorization
```shell
export API_TOKEN=sometoken
```
#### API_IP
While using API you can set this variable to accept request from a specific IP
```shell
export API_IP=127.0.0.1
```
#### MONITOR_YAML_PATH
```shell
export MONITOR_YAML_PATH=/your/path/monitors.yaml
```
#### SITE_YAML_PATH
```shell
export SITE_YAML_PATH=/your/path/site.yaml
```
If you do not specify MONITOR_YAML_PATH or SITE_YAML_PATH it will take the values from /config/site.yaml and /config/monitor.yaml respectively
## Production Deployment
```shell
export NODE_ENV=production
npm i
npm run build
npm run serve
```
It also needs 2 yaml files to work
- site.yaml: Contains information about the site
- monitors.yaml: Contains your monitors and their related specifications
By default these are present in `config/`. However you can use different location either passing them as argument or having the path as enviorment variable
### Add as Enviroment variables
```shell
export MONITOR_YAML_PATH=/your/path/monitors.yaml
export SITE_YAML_PATH=/your/path/site.yaml
```
### Add as argument to prod.js
```shell
npm run serve -- --monitors /your/path/monitors.yaml --site /your/path/site.yaml
```
## Install using Docker
[Dockerhub](https://hub.docker.com/r/rajnandan1/kener)
```shell
docker.io/rajnandan1/kener:latest
```
[Github Packages](https://github.com/rajnandan1/kener/pkgs/container/kener)
```shell
ghcr.io/rajnandan1/kener:latest
```
You should mount a host directory to persist your configuration and expose the web port. Environmental variables [found above](#environment-variable) can be passed with `-e` An example `docker run` command:
```shell
docker run -d -v /path/on/host/config:/config -p 3000:3000 -e "GH_TOKEN=1234" rajnandan1/kener
```
Or use **Docker Compose** with the example [docker-compose.yaml](docker-compose.yml)
### Using PUID and PGID
If you are
* running on a **linux host** (ie unraid) and
* **not** using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_)
then you must set the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid) in the container in order for it to generate files/folders your normal user can interact it.
Run these commands from your terminal
* `id -u` -- prints UID for **PUID**
* `id -g` -- prints GID for **PGID**
Then add to your docker command like so:
```shell
docker run -d ... -e "PUID=1000" -e "PGID=1000" ... rajnandan1/kener
```
or substitute them in [docker-compose.yml](/docker-compose.yml)
## Github Setup
Kener uses github for incident management. Issues created in github using certain tags go to kener as incidents.
### Step 1: Github Repositiory and Add to site.yaml
Create a Github Repositiory. It can be either public or private. After you have created a repository open `site.yaml` and add them like this
```yaml
github:
owner: "username"
repo: "respository"
```
### Step 2: Create Github Token
You can create either a classic token or personal access token
#### Creating Personal Access Token
- Go to [Personal Access Token](https://github.com/settings/personal-access-tokens/new)
- Token Name: kener
- Expiration: Use custom to select a calendar date
- Description: My Kener
- Repository access: Check Only Selected Repositories. Select your github repository
- Repository Permission: Select Issues Read Write
- Click on generate token
### Creating Classic Token
- Go to [Tokens](https://github.com/settings/tokens/new)
- Note: kener
- Expiration: No Expiration
- Scopes: write:packages
- Click on generate Token
Set the token as an environment variable
```shell
export GH_TOKEN=github_pat_11AD3ZA3Y0
```
---
# Modify Site
There is a folder called `/config`. Inside which there is a `site.yaml` file. You can modify this file to have your own branding.
```yaml
title: "Kener"
home: "/"
logo: "/logo.svg"
github:
owner: "rajnandan1"
repo: "kener"
incidentSince: 72
metaTags:
description: "Your description"
keywords: "keyword1, keyword2"
nav:
- name: "Documentation"
url: "/docs"
hero:
title: Kener is a Open-Source Status Page System
subtitle: Let your users know what's going on.
```
## title
This translates to
```html
Your Title
```
## theme
It can be set by modifying the `` class in `src/app.html` file
### Dark Theme
```html
```
### Light theme
```html
```
Can be `light` or `dark`. Defaults to `light`
## home
Location when someone clicks on the your brand in the top nav bar
```yaml
...
home: "https://www.example.com
...
```
## logo
URL of the logo that will be shown in the nav bar. You can also add your logo in the `static` folder
```yaml
...
logo: "https://www.example.com/logo.png
...
```
## favicon
It can be set by modifying the `` tag in `src/app.html` file.
Example add a png called `logo.png` file in `static/` and then
```html
...
...
```
## github
For incident kener uses github comments. Create an empty [github](https://github.com) repo and add them to `site.yaml`
```yaml
github:
owner: "username"
repo: "respository"
incidentSince: 72
```
`incidentSince` is in hours. It means if an issue is created before 72 hours then kener would not honor it. Default is 24
## metaTags
Meta tags are nothing but html ``. You can use them for SEO purposes
```yaml
metaTags:
description: "Your description"
keywords: "keyword1, keyword2"
og:image: "https://example.com/og.png"
```
will become
```html
```
## siteURL
You can set this to generate SiteMaps
```yaml
siteURL: https://kener.ing
```
Sitemaps urls will be `https://kener.ing/sitemap.xml`
## hero
Use hero to add a banner to your kener page
```yaml
hero:
title: Kener is a Open-Source Status Page System
subtitle: Let your users know what's going on.
```

## nav
You can add more links to your navbar.
```yaml
nav:
- name: "Home"
url: "/home"
```
## categories
You can define categories for your monitors. Each category can have a description. The monitors can be grouped by categories.
`name=home` will be shown in the home page. Categories are shown in the order they are defined in the yaml file. A dropdown will appear in the nav bar to select the category.
```yaml
categories:
- name: API
description: "Kener provides a simple API for you to use to update your status page."
- name: home
description: "lroem ipsum lorem ipsum"
```
## scripts
You can include any script in the `app.html` file like google analytics etc
---
# Add Monitors
Inside `config/` folder there is a file called `monitors.yaml`. We will be adding our monitors here. Please note that your yaml must be valid. It is an array.
## Understanding monitors
Each monitor runs at 1 minute interval by default. Monitor runs in below priorty order.
- defaultStatus Data
- API call Data overrides above data(if specified)
- Push Status Data overrides API Data
- Manual Incident Data overrides Pushed data
Sample
```yaml
- name: Google Search
description: Search the world's information, including webpages, images, videos and more.
tag: "google-search"
image: "/google.png"
cron: "* * * * *"
defaultStatus: "UP"
api:
timeout: 4000
method: POST
url: https://www.google.com/webhp
headers:
Content-Type: application/json
body: '{"order_amount":1,"order_currency":"INR"}'
eval: |
(function(statusCode, responseTime, responseDataBase64){
const resp = JSON.parse(atob(responseDataBase64));
return {
status: statusCode == 200 ? 'UP':'DOWN',
latency: responseTime,
}
})
```
| name | Required | This will be shown in the UI to your users. Keep it short and unique |
| ------------- | ----------------- | --------------------------------------------------------------------------------------------------------- |
| name | Required + Unique | This will be shown in the UI to your users. Keep it short and unique |
| description | Optional | This will be show below your name |
| tag | Required + Unique | This is used to tag incidents created in Github using comments |
| image | Optional | To show a logo before the name |
| cron | Optinal | Use cron expression to specify the interval to run the monitors. Defaults to `* * * * *` i.e every minute |
| api.method | Optional | HTTP Method |
| api.url | Optional | HTTP URL |
| api.headers | Optional | HTTP headers |
| api.body | Optional | HTTP Body as string |
| api.eval | Optional | Evaluator written in JS, to parse HTTP response and calculate uptime and latency |
| defaultStatus | Optional | If no API is given this will be the default status. can be UP/DOWN/DEGRADED |
| hidden | Optional | If set to `true` will not show the monitor in the UI |
| category | Optional | Use this to group your monitors. Make sure you have defined category in `site.yaml` and use the `name` attribute here |
## cron
Kener fills data every minute in UTC so if you give an expression that is not per minute, kener will backfill data using the latest status.
Example for `cron: "*/15 * * * *"`
- First run at "2023-12-02T18:00:00.000Z" - Status DOWN
- Second run at "2023-12-02T18:15:00.000Z" - Status UP
Kener will fill data from 18:01:00 to 18:14:00 as UP
## eval
This is a anonymous JS function, by default it looks like this.
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and lantecy(number)
`{status:"DEGRADED", latency: 200}`.
```js
(function (statusCode, responseTime, responseDataBase64) {
let statusCodeShort = Math.floor(statusCode/100);
let status = 'DOWN'
if(statusCodeShort >=2 && statusCodeShort <= 3) {
status = 'UP',
}
return {
status: 'DOWN',
latency: responseTime,
}
})
```
- `statusCode` **REQUIRED** is a number. It is the HTTP status code
- `responseTime` **REQUIRED**is a number. It is the latency in milliseconds
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it
```js
let decodedResp = atob(responseDataBase64);
//let jsonResp = JSON.parse(decodedResp)
```
---
# Monitor Examples
Here are some exhaustive examples for monitors
## A Simple GET Monitor
```yaml
- name: Google Search
tag: "google-search"
api:
method: GET
url: https://www.google.com/webhp
```
## A GET Monitor with image
google.png is in the static folder
```yaml
- name: Google Search
tag: "google-search"
image: "/google.png"
api:
method: GET
url: https://www.google.com/webhp
```
## Get Monitor 15 Minute
```yaml
- name: Google Search
description: Search the world's information, including webpages, images, videos and more.
tag: "google-search"
cron: "*/15 * * * *"
api:
method: GET
url: https://www.google.com/webhp
```
## Post Monitor With Body
```yaml
- name: Google Search
description: Google Search
tag: "google-search-post"
api:
method: POST
url: https://www.google.com/webhp
headers:
Content-Type: application/json
body: '{"order_amount":22222.1,"order_currency":"INR"}'
```
## Secrets in Header
You can set ENV variables in your machine and use them in your monitors. Example below has `GH_TOKEN` as an environment variable. It uses process.env.GH_TOKEN.
`export GH_TOKEN=some.token.for.github`
> **_NOTE:_** DO NOT forget the `$` sign in your monitor
```yaml
- name: Github Issues
description: Github Issues Fetch
tag: "gh-search-issue"
api:
method: GET
url: https://api.github.com/repos/rajnandan1/kener/issues
headers:
Authorization: Bearer $GH_TOKEN
```
## Secrets in Body
Assuming `ORDER_ID` is present in env
```yaml
- name: Github Issues
description: Github Issues Fetch
tag: "gh-search-issue"
api:
method: POST
url: https://api.github.com/repos/rajnandan1/kener/issues
headers:
Content-Type: application/json
body: '{"order_amount":22222.1,"order_currency":"INR", "order_id": "$ORDER_ID"}'
```
## Eval Body
```yaml
- name: Github Issues
description: Github Issues Fetch
tag: "gh-search-issue"
api:
method: GET
url: https://api.github.com/repos/rajnandan1/kener/issues
eval: |
(function(statusCode, responseTime, responseDataBase64){
const resp = JSON.parse(atob(responseDataBase64));
let status = 'DOWN'
if(statusCode == 200) status = 'UP';
if(resp.length == 0) status = 'DOWN';
if(statusCode == 200 && responseTime > 2000) status = 'DEGRADED';
return {
status: status,
latency: responseTime,
}
})
```
## With defaultStatus UP
Each minute it will set the status as UP
```yaml
- name: Earth
description: Our Planent
tag: "earth"
defaultStatus: UP
```
## With Category
Add this monitor to the API category instead of the default home category
```yaml
- name: Earth
description: Our Planent
tag: "earth"
category: API
```
---
# Incident Management
Kener uses Github to power incident management using labels
## Labels
Kener auto creates labels for your monitors using the `tag` parameter
- `incident`: If an issue is marked as incident it will show up in kener home page
- `incident-down`: If an issue is marked as incident-down and incident kener would make that monitor down
- `incident-degraded`: If an issue is marked as incident-degraded and incident then kener would make the monitor degraded
- `resolved`: Use this tag to mark the incident has RESOLVED
- `identified`: Use this tag to show that the root cause of the incident has been identified
## Creating your first incident
- Go to your github repo of kener
- Go to issues
- Create an issue. Give it a title
- In the body add [start_datetime:1702651340] and [end_datetime:1702651140] and add some description. Time is UTC
- Add `incident`, `incident-down` and the monitor tag. This will make the monitor down for 4 minutes
Here is a [sample incident](https://github.com/rajnandan1/kener/issues/15) for your reference.
---
# API
Kener also gives APIs to push data and create incident. Before you use kener apis you will have to set an authorization token called `API_TOKEN`. This also has to be set as an environment variable.
```shell
export API_TOKEN=some-token-set-by-you
```
Additonally you can set IP whitelisting by setting another environment token called `API_IP`
```shell
export API_IP=127.0.0.1
```
## Update Status
The update status API can be used to manually update the state of a monitor from a remote server.
### Request Body
| Parameter | Description |
| ------------------ | ------------------------------------------------------------------------------------ |
| status | `Required` Can be only UP/DOWN/DEGRADED |
| latency | `Required` In Seconds. Leave 0 if not required |
| timestampInSeconds | `Optional` Timestamp in UTC seconds. Defaults to now. Should between 90 Days and now |
| tag | `Required` Monitor Tag set in monitors.yaml |
```shell
curl --request POST \
--url http://your-kener.host/api/status \
--header 'Authorization: Bearer some-token-set-by-you' \
--header 'Content-Type: application/json' \
--data '{
"status": "DOWN",
"latency": 1213,
"timestampInSeconds": 1702405860,
"tag": "google-search"
}'
```
### Response
```json
{
"status": 200,
"message": "success at 1702405860"
}
```
This will update the status of the monitor with tag `google-search` to DOWN at UTC 1702405860
## Create an Incident
Can be use to create an incident from a remote server
### Request Body
| Parameter | Description |
| ------------------ | ------------------------------------------------------------------------------------ |
| startDatetime | `Optional` When did the incident start in UTC second |
| endDatetime | `Optional` When did the incident end in UTC seconds |
| title | `Required` Title of the incident |
| body | `Optional` Body of the incident |
| tags | `Required` Array of String, Monitor Tags of the incident |
| impact | `Optional` Can be only DOWN/DEGRADED |
| isMaintenance | `Optional` Boolean if incident is a maitainance |
| isIdentified | `Optional` Incident identified |
| isResolved | `Optional` Incident resolved |
```shell
curl --request POST \
--url http://your-kener.host/api/incident \
--header 'Authorization: Bearer some-token-set-by-you' \
--header 'Content-Type: application/json' \
--data '{
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"title": "Outage in Mumbai",
"body": "Login cluster is down in mumbai region",
"tags": ["google-search"],
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}'
```
### Response
```json
{
"createdAt": 1703940450,
"closedAt": null,
"title": "Outage in Mumbai",
"tags": ["google-search"],
"incidentNumber": 12,
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"body": "Login cluster is down in mumbai region",
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}
```
## Update an Incident
Can be use to update an incident from a remote server. It will clear values if not passed
### Request Param
- `incidentNumber`: Number of the incident
### Request Body
| Parameter | Description |
| ------------------ | ------------------------------------------------------------------------------------ |
| startDatetime | `Optional` When did the incident start in UTC second |
| endDatetime | `Optional` When did the incident end in UTC seconds |
| title | `Required` Title of the incident |
| body | `Optional` Body of the incident |
| tags | `Required` Array of String, Monitor Tags of the incident |
| impact | `Optional` Can be only DOWN/DEGRADED |
| isMaintenance | `Optional` Boolean if incident is a maitainance |
| isIdentified | `Optional` Incident identified |
| isResolved | `Optional` Incident resolved |
```shell
curl --request PATCH \
--url http://your-kener.host/api/incident/{incidentNumber} \
--header 'Authorization: Bearer some-token-set-by-you' \
--header 'Content-Type: application/json' \
--data '{
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"title": "Outage in Mumbai",
"body": "Login cluster is down in mumbai region",
"tags": ["google-search"],
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}'
```
### Response
```json
{
"createdAt": 1703940450,
"closedAt": null,
"title": "Outage in Mumbai",
"tags": ["google-search"],
"incidentNumber": 12,
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"body": "Login cluster is down in mumbai region",
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}
```
## Get an Incident
Use `incidentNumber` to fetch an incident
### Request Body
```shell
curl --request GET \
--url http://your-kener.host/api/incident/{incidentNumber} \
--header 'Authorization: Bearer some-token-set-by-you' \
```
### Response
```json
{
"createdAt": 1703940450,
"closedAt": null,
"title": "Outage in Mumbai",
"tags": ["google-search"],
"incidentNumber": 12,
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"body": "Login cluster is down in mumbai region",
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}
```
## Add Comment
Add comments for incident using `incidentNumber`
### Request
```shell
curl --request POST \
--url http://your-kener.host/api/incident/{incidentNumber}/comment \
--header 'Authorization: Bearer some-token-set-by-you' \
--header 'Content-Type: application/json' \
--data '{
"body": "comment 1"
}'
```
### Response
```json
{
"commentID": 1873376745,
"body": "comment 1",
"createdAt": 1704123938
}
```
## Get Comments
Use this API to fetch all the comments for an incident
### Request
```shell
curl --request GET \
--url http://your-kener.host/api/incident/{incidentNumber}/comment \
--header 'Authorization: Bearer some-token-set-by-you' \
```
### Response
```json
[
{
"commentID": 1873372042,
"body": "comment 1",
"createdAt": 1704123116
},
{
"commentID": 1873372169,
"body": "comment 2",
"createdAt": 1704123139
}
]
```
## Update Incident Status
Use this to API to update the status of an ongoing incident.
### Request Body
| Parameter | Description |
| ------------------ | ------------------------------------------------------------------------------------ |
| isIdentified | `Optional` Boolean, set it when incident has been identified |
| isResolved | `Optional` Boolean, set it when incident has been resolved |
| endDatetime | `Optional` When did the incident end in UTC seconds |
### Request
```shell
curl --request POST \
--url http://your-kener.host/api/incident/{incidentNumber}/status \
--header 'Authorization: Bearer some-token-set-by-you' \
--header 'Content-Type: application/json' \
--data '{
"isIdentified": true,
"isResolved": false
"endDatetime": 1702405920
}'
```
### Response
```json
{
"createdAt": 1703940450,
"closedAt": null,
"title": "Outage in Mumbai",
"tags": ["google-search"],
"incidentNumber": 12,
"startDatetime": 1702405740,
"endDatetime": 1702405920,
"body": "Login cluster is down in mumbai region",
"impact": "DOWN",
"isMaintenance": false,
"isIdentified": true,
"isResolved": false
}
```
---
# Badge
There are two types of badges
Syntax
```md
http://[hostname]/badge/[tag]/status
http://[hostname]/badge/[tag]/uptime
```
## Status
Shows the last health check was UP/DOWN/DEGRADED

Example in HTML
```html
```
Example in MarkDown
```md

```
## Uptime
Shows the 90 Day uptime by default. You can `sinceLast` as query param to get uptime since last x seconds.

### 90 Day Uptime
Example in HTML
```html
```
Example in MarkDown
```md

```
### 15 Minute Uptime
Example in HTML
```html
```
Example in MarkDown
```md

```
## Customize Badges
You can set different colors for badges and style.
### With Custom Label Color

```md

```
### With Custom Value Color

```md

```
### With Both Different Colors

```md

```
### Style Of the Badge
You can change the style of the badge. Supported Styles are `plastic`, `flat`, `flat-square`, `for-the-badge` or `social`. Default is `flat`
#### plastic

```md

```
#### flat

```md

```
#### flat-square

```md

```
#### for-the-badge

```md

```
#### social

```md

```