diff --git a/.gitignore b/.gitignore
index caa9c131..21bb28d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ static/kener
build
config/monitors.yaml
config/site.yaml
+config/server.yaml
/.svelte-kit
/src/lib/.kener
/package
@@ -19,6 +20,4 @@ config/static/*
db/*
!db/.kener
database/*
-!database/.kener
-src/lib/server/config/monitors.yaml
-src/lib/server/config/site.yaml
\ No newline at end of file
+!database/.kener
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 0c1b88f4..f8deb0d1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,19 @@
FROM lsiobase/alpine:3.18 as base
-ENV TZ=Etc/GMT
+ENV TZ=Etc/UTC
RUN \
echo "**** install build packages ****" && \
apk add --no-cache \
nodejs \
- npm && \
+ npm \
+ python3 \
+ make \
+ gcc \
+ g++ \
+ sqlite \
+ sqlite-dev \
+ libc6-compat && \
echo "**** cleanup ****" && \
rm -rf \
/root/.cache \
@@ -15,12 +22,12 @@ RUN \
# set OS timezone specified by docker ENV
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
-
+# ARG data_dir=/config
+# VOLUME $data_dir
+# ENV CONFIG_DIR=$data_dir
COPY docker/root/ /
-
-
# build requires devDependencies which are not used by production deploy
# so build in a stage so we can copy results to clean "deploy" stage later
FROM base as build
@@ -29,7 +36,10 @@ WORKDIR /app
COPY --chown=abc:abc . /app
-RUN npm install \
+RUN \
+ npm install node-gyp -g && \
+ npm_config_build_from_source=true npm install better-sqlite3 && \
+ npm install \
&& chown -R root:root node_modules \
&& npm run build
@@ -47,15 +57,13 @@ COPY --chown=abc:abc build.js /app/build.js
COPY --chown=abc:abc sitemap.js /app/sitemap.js
COPY --chown=abc:abc openapi.json /app/openapi.json
COPY --chown=abc:abc src/lib/server /app/src/lib/server
-COPY --chown=abc:abc src/lib/helpers.js /app/src/lib/helpers.js
COPY --from=build --chown=abc:abc /app/build /app/build
COPY --from=build --chown=abc:abc /app/main.js /app/main.js
-
ENV NODE_ENV=production
-# install prod depdendencies and clean cache
+# install prod dependencies and clean cache
RUN npm install --omit=dev \
&& npm cache clean --force \
&& chown -R abc:abc node_modules
@@ -67,4 +75,4 @@ EXPOSE $PORT
# leave entrypoint blank!
# uses LSIO s6-init entrypoint with scripts
# that populate CONFIG_DIR with static dir, monitor/site.yaml when dir is empty
-# and chown's all files so they are owned by proper user based on PUID/GUID env
+# and chown's all files so they are owned by proper user based on PUID/GUID env
\ No newline at end of file
diff --git a/README.md b/README.md
index a8529701..b439f423 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,6 @@
-
#### [π Visit a live server](https://kener.ing)
#### [π Quick Start](https://kener.ing/docs/quick-start)
@@ -35,9 +34,9 @@
## What is Kener?
-Kener: Open-source sveltekit status page tool, designed to make service monitoring and incident handling a breeze. It offers a sleek and user-friendly interface that simplifies tracking service outages and improves how we communicate during incidents. Kener integrates seamlessly with GitHub, making incident management a team effortβmaking.
+Kener: Open-source sveltekit status page system, crafted with lot of thought so that it looks modern.
-It uses files to store the data.
+It does not aim to replace the Datadogs of the world. It simply tries to help someone come with a status page for the world.
Kener name is derived from the word "Kene" which means "how is it going" in Assamese, then .ing is added to make cooler.
@@ -60,6 +59,7 @@ Here are some of the features that you get out of the box. Please read the docum
- Supports a Default Status for Monitors. Example defaultStatus=DOWN if you don't hit API per minute with Status UP
- Supports base path for hosting in k8s
- Pre-built docker image for easy deployment
+- Supports webhooks/discord/slack for notifications
### Customization and Branding
diff --git a/build.js b/build.js
index 45d38ecf..323570b0 100644
--- a/build.js
+++ b/build.js
@@ -4,11 +4,14 @@ import axios from "axios";
import {
IsValidURL,
checkIfDuplicateExists,
- getWordsStartingWithDollar,
IsValidHTTPMethod,
- ValidateIpAddress
+ ValidateIpAddress,
+ IsValidHost,
+ IsValidRecordType,
+ IsValidNameServer
} from "./src/lib/server/tool.js";
import { API_TIMEOUT, AnalyticsProviders } from "./src/lib/server/constants.js";
+import { GetAllGHLabels, CreateGHLabel } from "./src/lib/server/github.js";
const configPathFolder = "./config";
const databaseFolder = process.argv[2] || "./database";
@@ -25,9 +28,55 @@ const defaultEval = `(function (statusCode, responseTime, responseData) {
latency: responseTime,
}
})`;
+
+function validateServerFile(server) {
+ //if empty return true
+ if (Object.keys(server).length === 0) {
+ return true;
+ }
+ //server.triggers is present then it should be an array
+ if (server.triggers !== undefined && !Array.isArray(server.triggers)) {
+ console.log("triggers should be an array");
+ return false;
+ }
+ ///each trigger should have a name, type, and url
+ if (server.triggers !== undefined) {
+ for (let i = 0; i < server.triggers.length; i++) {
+ const trigger = server.triggers[i];
+ if (
+ trigger.name === undefined ||
+ trigger.type === undefined ||
+ trigger.url === undefined
+ ) {
+ console.log("trigger should have name, type, and url");
+ return false;
+ }
+ }
+ }
+ //if database is present then it should be an object, and they key can be either postgres or sqlite
+ if (server.database !== undefined && typeof server.database !== "object") {
+ console.log("database should be an object");
+ return false;
+ }
+ if (server.database !== undefined) {
+ let dbtype = Object.keys(server.database);
+ if (dbtype.length !== 1) {
+ console.log("database should have only one key");
+ return false;
+ }
+ if (dbtype[0] !== "postgres" && dbtype[0] !== "sqlite") {
+ console.log("database should be either postgres or sqlite");
+ return false;
+ }
+ }
+
+ return true;
+}
+
async function Build() {
- console.log("Building Kener...");
+ console.log("βΉοΈ Building Kener...");
let site = {};
+ let server = {};
let monitors = [];
try {
site = yaml.load(fs.readFileSync(configPathFolder + "/site.yaml", "utf8"));
@@ -36,6 +85,15 @@ async function Build() {
console.log(error);
process.exit(1);
}
+ try {
+ server = yaml.load(fs.readFileSync(configPathFolder + "/server.yaml", "utf8"));
+ if (!validateServerFile(server)) {
+ process.exit(1);
+ }
+ } catch (error) {
+ console.warn("server.yaml not found");
+ server = {};
+ }
if (
site.github === undefined ||
@@ -43,12 +101,23 @@ async function Build() {
site.github.repo === undefined
) {
console.log("github owner and repo are required");
- process.exit(1);
+ site.hasGithub = false;
+ // process.exit(1);
+ } else {
+ site.hasGithub = true;
+ }
+
+ if (site.hasGithub && !!!site.github.incidentSince) {
+ site.github.incidentSince = 720;
+ }
+ if (site.hasGithub && !!!site.github.apiURL) {
+ site.github.apiURL = "https://api.github.com";
}
const FOLDER_DB = databaseFolder;
const FOLDER_SITE = FOLDER_DB + "/site.json";
const FOLDER_MONITOR = FOLDER_DB + "/monitors.json";
+ const FOLDER_SERVER = FOLDER_DB + "/server.json";
for (let i = 0; i < monitors.length; i++) {
const monitor = monitors[i];
@@ -57,6 +126,7 @@ async function Build() {
let tag = monitor.tag;
let hasAPI = monitor.api !== undefined && monitor.api !== null;
let hasPing = monitor.ping !== undefined && monitor.ping !== null;
+ let hasDNS = monitor.dns !== undefined && monitor.dns !== null;
let folderName = name.replace(/[^a-z0-9]/gi, "-").toLowerCase();
monitors[i].folderName = folderName;
@@ -121,6 +191,44 @@ async function Build() {
}
monitors[i].hasPing = true;
}
+ if (hasDNS) {
+ let dnsData = monitor.dns;
+ let domain = dnsData.host;
+ //check if domain is valid
+ if (!!!domain || !IsValidHost(domain)) {
+ console.log("domain is not valid");
+ process.exit(1);
+ }
+
+ let recordType = dnsData.lookupRecord;
+ //check if recordType is valid
+ if (!!!recordType || !IsValidRecordType(recordType)) {
+ console.log("recordType is not valid");
+ process.exit(1);
+ }
+
+ let nameServer = dnsData.nameServer;
+ //check if nameserver is valid
+ if (!!nameServer && !IsValidNameServer(nameServer)) {
+ console.log("nameServer is not valid");
+ process.exit(1);
+ }
+
+ // matchType: "ANY" # ANY, ALL
+ let matchType = dnsData.matchType;
+ if (!!!matchType || (matchType !== "ANY" && matchType !== "ALL")) {
+ console.log("matchType is not valid");
+ process.exit(1);
+ }
+
+ //values array of string at least one
+ let values = dnsData.values;
+ if (!!!values || !Array.isArray(values) || values.length === 0) {
+ console.log("values is not valid");
+ process.exit(1);
+ }
+ monitors[i].hasDNS = true;
+ }
if (hasAPI) {
let url = monitor.api.url;
let method = monitor.api.method;
@@ -222,35 +330,14 @@ async function Build() {
}
}
} catch (error) {
- console.log(error);
+ console.log(`error while fetching ${url}`);
}
}
}
- monitors[i].path0Day = `${FOLDER_DB}/${folderName}.0day.utc.json`;
- monitors[i].path90Day = `${FOLDER_DB}/${folderName}.90day.utc.json`;
monitors[i].hasAPI = hasAPI;
-
- //secrets can be in url/body/headers
- //match in monitor.url if a words starts with $, get the word
- const requiredSecrets = getWordsStartingWithDollar(
- `${monitor.url} ${monitor.body} ${JSON.stringify(monitor.headers)}`
- ).map((x) => x.substr(1));
-
- //iterate over process.env
- for (const [key, value] of Object.entries(process.env)) {
- if (requiredSecrets.indexOf(key) !== -1) {
- envSecrets.push({
- find: `$${key}`,
- replace: value
- });
- }
- }
}
- if (site.github.incidentSince === undefined || site.github.incidentSince === null) {
- site.github.incidentSince = 720;
- }
if (site.siteName === undefined) {
site.siteName = site.title;
}
@@ -260,6 +347,22 @@ async function Build() {
if (site.themeToggle === undefined) {
site.themeToggle = true;
}
+ if (site.barStyle === undefined) {
+ site.barStyle = "FULL";
+ }
+ if (site.barRoundness === undefined) {
+ site.barRoundness = "ROUNDED";
+ } else {
+ site.barRoundness = site.barRoundness.toLowerCase();
+ }
+ if (site.summaryStyle === undefined) {
+ site.summaryStyle = "DAY";
+ }
+ site.colors = {
+ UP: site.colors?.UP || "#4ead94",
+ DOWN: site.colors?.DOWN || "#ca3038",
+ DEGRADED: site.colors?.DEGRADED || "#e6ca61"
+ };
if (!!site.analytics) {
const providers = {};
@@ -293,14 +396,65 @@ async function Build() {
fs.ensureFileSync(FOLDER_MONITOR);
fs.ensureFileSync(FOLDER_SITE);
-
+ fs.ensureFileSync(FOLDER_SERVER);
try {
fs.writeFileSync(FOLDER_MONITOR, JSON.stringify(monitors, null, 4));
fs.writeFileSync(FOLDER_SITE, JSON.stringify(site, null, 4));
+ fs.writeFileSync(FOLDER_SERVER, JSON.stringify(server, null, 4));
} catch (error) {
console.log(error);
process.exit(1);
}
+
+ console.log("β Kener built successfully");
+
+ if (site.hasGithub) {
+ const ghLabels = await GetAllGHLabels(site);
+ const tagsAndDescription = monitors.map((monitor) => {
+ return { tag: monitor.tag, description: monitor.name };
+ });
+ //add incident label if does not exist
+
+ if (ghLabels.indexOf("incident") === -1) {
+ await CreateGHLabel(site, "incident", "Status of the site");
+ }
+ if (ghLabels.indexOf("resolved") === -1) {
+ await CreateGHLabel(site, "resolved", "Incident is resolved", "65dba6");
+ }
+ if (ghLabels.indexOf("identified") === -1) {
+ await CreateGHLabel(site, "identified", "Incident is Identified", "EBE3D5");
+ }
+ if (ghLabels.indexOf("manual") === -1) {
+ await CreateGHLabel(site, "manual", "Manually Created Incident", "6499E9");
+ }
+ if (ghLabels.indexOf("auto") === -1) {
+ await CreateGHLabel(site, "auto", "Automatically Created Incident", "D6C0B3");
+ }
+ if (ghLabels.indexOf("investigating") === -1) {
+ await CreateGHLabel(site, "investigating", "Incident is investigated", "D4E2D4");
+ }
+ if (ghLabels.indexOf("incident-degraded") === -1) {
+ await CreateGHLabel(
+ site,
+ "incident-degraded",
+ "Status is degraded of the site",
+ "f5ba60"
+ );
+ }
+ if (ghLabels.indexOf("incident-down") === -1) {
+ await CreateGHLabel(site, "incident-down", "Status is down of the site", "ea3462");
+ }
+ //add tags if does not exist
+ for (let i = 0; i < tagsAndDescription.length; i++) {
+ const tag = tagsAndDescription[i].tag;
+ const description = tagsAndDescription[i].description;
+ if (ghLabels.indexOf(tag) === -1) {
+ await CreateGHLabel(site, tag, description);
+ }
+ }
+
+ console.log("β Github labels created successfully");
+ }
}
Build();
diff --git a/config/monitors.example.yaml b/config/monitors.example.yaml
index f6819498..ed5b0880 100644
--- a/config/monitors.example.yaml
+++ b/config/monitors.example.yaml
@@ -17,3 +17,32 @@
api:
method: GET
url: https://www.frogment.com
+ alerts:
+ DOWN:
+ failureThreshold: 5
+ successThreshold: 2
+ createIncident: false
+ description: "Write a description here please"
+ triggers:
+ - MyWebhook
+ - Discord Test
+- name: CNAME Lookup
+ description: Monitor example showing how to lookup CNAME record for a domain. The site www.rajnandan.com is hosted on GitHub Pages.
+ tag: "cname-rajnandan"
+ image: "https://www.rajnandan.com/assets/images/me.jpg"
+ defaultStatus: "UP"
+ dns:
+ host: "www.rajnandan.com"
+ lookupRecord: "CNAME"
+ nameServer: "8.8.8.8"
+ matchType: "ANY" # ANY, ALL
+ values:
+ - "rajnandan1.github.io"
+- name: "Frogment APP Ping"
+ description: "Ping www.frogment.app"
+ image: https://www.frogment.app/icons/Square107x107Logo.png
+ tag: "pingFrogmentApp"
+ defaultStatus: "UP"
+ ping:
+ hostsV4:
+ - "www.frogment.app"
\ No newline at end of file
diff --git a/config/server.example.yaml b/config/server.example.yaml
new file mode 100644
index 00000000..4ed8f8cf
--- /dev/null
+++ b/config/server.example.yaml
@@ -0,0 +1,13 @@
+triggers:
+ - name: MyWebhook
+ type: webhook
+ url: https://kener.requestcatcher.com/test
+ method: POST
+ headers:
+ Authorization: Bearer SomeToken
+ - name: Discord Test
+ type: discord
+ url: https://discord.com/api/webhooks/1310641119767302164/XJvq4MO2lz5yp9XRCJgfc4dbUfcQdHsttFUKTFJx4y_Oo1jNkUXf-CS3RSnamnNNv4Lx
+database:
+ sqlite:
+ dbName: kener.db
\ No newline at end of file
diff --git a/config/site.example.yaml b/config/site.example.yaml
index b5d1f16c..5b57d1db 100644
--- a/config/site.example.yaml
+++ b/config/site.example.yaml
@@ -52,4 +52,7 @@ pattern: "squares"
analytics:
- id: "G-Q3MLRXCBFT"
type: "GA"
+barRoundness: SHARP
+summaryStyle: CURRENT
+barStyle: PARTIAL
diff --git a/delay.js b/delay.js
new file mode 100644
index 00000000..1c0ce17e
--- /dev/null
+++ b/delay.js
@@ -0,0 +1,40 @@
+import fs from "fs-extra";
+import path from "path";
+
+let maxWait = 5000;
+let interval = 1000;
+let waitTime = 0;
+let serverDataPath = path.join(process.cwd(), "database", "server.json");
+let siteDataPath = path.join(process.cwd(), "database", "site.json");
+let monitorsDataPath = path.join(process.cwd(), "database", "monitors.json");
+
+function allFilesExist() {
+ return (
+ fs.existsSync(serverDataPath) &&
+ fs.existsSync(siteDataPath) &&
+ fs.existsSync(monitorsDataPath)
+ );
+}
+
+//use setTimeout to create a delay promise
+function delay(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+let requiredFilesExist = allFilesExist();
+
+//create anonymous function to call the init function
+(async function init() {
+ while (!requiredFilesExist && waitTime < maxWait) {
+ await delay(1000);
+ requiredFilesExist = allFilesExist();
+
+ waitTime += interval;
+ }
+ if (!requiredFilesExist) {
+ console.error("Error loading site data");
+ process.exit(1);
+ } else {
+ console.log("β All files exist. Starting Frontend server...");
+ }
+})();
diff --git a/docker-compose.yml b/docker-compose.yml
index d33da656..c5c9ef62 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,7 +5,7 @@ services:
container_name: kener-rc
#env_file: .env #uncomment this, if you are using .env file
environment:
- - TZ=Etc/GMT
+ - TZ=Etc/UTC
#- GH_TOKEN=
#- API_TOKEN=
#- API_IP=
diff --git a/docker/root/etc/s6-overlay/s6-rc.d/init-app-config/run b/docker/root/etc/s6-overlay/s6-rc.d/init-app-config/run
index a6791d9e..1f4a731b 100755
--- a/docker/root/etc/s6-overlay/s6-rc.d/init-app-config/run
+++ b/docker/root/etc/s6-overlay/s6-rc.d/init-app-config/run
@@ -25,8 +25,8 @@ if [ "$POPULATE_EXAMPLES" = true ]; then
echo "Directory is empty, adding defaults..."
mkdir -p "${CONFIG_DIR}"/static
cp -r /app/static/. "${CONFIG_DIR}"/static
- mv /app/config/monitors.example.yaml "${CONFIG_DIR}"/monitors.yaml
- mv /app/config/site.example.yaml "${CONFIG_DIR}"/site.yaml
+ cp /app/config/monitors.example.yaml "${CONFIG_DIR}"/monitors.yaml
+ cp /app/config/site.example.yaml "${CONFIG_DIR}"/site.yaml
fi
# permissions
diff --git a/docs/alerting.md b/docs/alerting.md
new file mode 100644
index 00000000..190ac254
--- /dev/null
+++ b/docs/alerting.md
@@ -0,0 +1,160 @@
+---
+title: Alert Config | Server.yaml | Kener
+description: Alerts are the heart of Kener. This is where you define the alerts you want to show on your site.
+---
+
+# Add Alert Config
+
+Use the `config/server.yaml` file to configure the alert settings.
+
+## Alert Triggers
+
+Kener supports multiple alerting mechanisms.
+
+Currently, Kener supports the following alerting mechanisms:
+
+- Webhook
+- Discord
+- Slack
+
+We are adding more alerting mechanisms in the future.
+
+### Configure Alerts
+
+In the `server.yaml` file, you can add the alerting mechanisms under the `triggers` key. It accepts an array of objects.
+
+```yaml
+triggers:
+ - name: Awesome Webhook
+ type: "webhook"
+ url: "https://kener.requestcatcher.com/test"
+ method: "POST"
+ headers:
+ Authorization: Bearer $SOME_TOKEN_FROM_ENV
+ - name: My Discord Channel
+ type: "discord"
+ url: "https://discord.com/api/webhooks/your-webhook-url"
+ - name: Some Slack Channel
+ type: slack
+ url: https://hooks.slack.com/services/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
+```
+
+| Key | Description |
+| ---- | ----------------------------------------------------------------------- |
+| name | Name of the alerting mechanism. This will be used in the monitor config |
+| type | Type of the alerting mechanism. Can be `webhook`, `discord`, `slack` |
+| url | URL of the webhook or discord or slack channel |
+
+There may be additional keys based on the type of alerting mechanism.
+
+In webhook alerting, you can also add headers and method to the request.
+
+```yaml
+- name: Awesome Webhook
+ type: "webhook"
+ url: "https://kener.requestcatcher.com/test"
+ method: "POST"
+ headers:
+ Authorization: Bearer $SOME_TOKEN_FROM_ENV
+```
+
+### Webhook
+
+Body of the webhook will be sent as below:
+
+```json
+{
+ "id": "mockoon-9",
+ "alert_name": "Mockoon DOWN",
+ "severity": "critical",
+ "status": "TRIGGERED",
+ "source": "Kener",
+ "timestamp": "2024-11-27T04:55:00.369Z",
+ "description": "π¨ **Service Alert**: Check the details below",
+ "details": {
+ "metric": "Mockoon",
+ "current_value": 1,
+ "threshold": 1
+ },
+ "actions": [
+ {
+ "text": "View Monitor",
+ "url": "https://kener.ing/monitor-mockoon"
+ }
+ ]
+}
+```
+
+| Key | Description |
+| --------------------- | ----------------------------------------------------------- |
+| id | Unique ID of the alert |
+| alert_name | Name of the alert |
+| severity | Severity of the alert. Can be `critical`, `warn` |
+| status | Status of the alert. Can be `TRIGGERED`, `RESOLVED` |
+| source | Source of the alert. Can be `Kener` |
+| timestamp | Timestamp of the alert |
+| description | Description of the alert. This you can customize. See below |
+| details | Details of the alert. |
+| details.metric | Name of the monitor |
+| details.current_value | Current value of the monitor |
+| details.threshold | Alert trigger hreshold of the monitor |
+| actions | Actions to be taken. Link to view the monitor. |
+
+### Discord
+
+The discord message when alert is `TRIGGERED` will look like this
+
+![Discord](/discord.png)
+
+The discord message when alert is `RESOLVED` will look like this
+
+![Discord](/discord_resolved.png)
+
+### Slack
+
+The slack message when alert is `TRIGGERED` will look like this
+
+![Slack](/slack.png)
+
+The slack message when alert is `RESOLVED` will look like this
+
+![Slack](/slack_resolved.png)
+
+### Add Alerts to Monitors
+
+Once you have set up the triggers, you can add them to your monitors in the `config/monitors.yaml` file.
+
+```yaml
+- name: OkBookmarks
+ tag: "okbookmarks"
+ image: "https://okbookmarks.com/assets/img/extension_icon128.png"
+ api:
+ method: GET
+ url: https://okbookmarks.com
+ alerts:
+ DOWN:
+ failureThreshold: 1
+ successThreshold: 1
+ createIncident: true
+ description: "π¨ **Service Alert**. This is a custom message"
+ triggers:
+ - Awesome Webhook
+ - My Discord Channel
+ - Some Slack Channel
+```
+
+The `alerting` object lets you define the alerting mechanism for the monitor. It can do alerting for `DOWN` or `DEGRADED` status. You can add both or one of them.
+
+- `failureThreshold`: Number of consecutive failures before alerting
+- `successThreshold`: Number of consecutive successes before resolving the alert
+- `createIncident`: If set to `true`, Kener will create an incident that will be shown on the status page
+- `description`: Custom message for the alert
+- `triggers`: Array of alerting triggers to send the alert to. The name should match the name in the `server.yaml` file.
+
+
+
+ It will send alerts to the webhook, discord, and slack channels.
+ The alert will be set when the monitor goes down for 1 health check.
+ There will be one more alert when the monitor is up again after, 1 health check is successful.
+
+
diff --git a/docs/changelogs.md b/docs/changelogs.md
index 75543bda..ba3e7706 100644
--- a/docs/changelogs.md
+++ b/docs/changelogs.md
@@ -5,6 +5,30 @@ description: Changelogs for Kener
# Changelogs
+## v2.0.0
+
+
+
+Here are the changes in this release
+
+### Features
+
+- Added support for sqlite3 and removed dependency on file system
+- Added support for postgres database. Read more [here](/docs/database)
+- Added support for alerting. Read more [here](/docs/alerting)
+- Added color customization. Read more [here](/docs/customize-site#color)
+- Added three new customizations for home page. Read more [here](/docs/customize-site#barstyle)
+ - `barStyle`
+ - `barRoundness`
+ - `summaryStyle`
+
+### Migration
+
+Kener will automatically migrate your data from file system to sqlite3. If you are using a custom domain, you need to update the `site.yaml` file with the new `siteURL` field. Read more [here](/docs/customize-site#siteURL)
+
## v0.0.16