diff --git a/commafeed-client/src/App.tsx b/commafeed-client/src/App.tsx
index f55c1872..61b71733 100644
--- a/commafeed-client/src/App.tsx
+++ b/commafeed-client/src/App.tsx
@@ -14,6 +14,7 @@ import { Header } from "components/header/Header"
import { Tree } from "components/sidebar/Tree"
import { useI18n } from "i18n"
import { AdminUsersPage } from "pages/admin/AdminUsersPage"
+import { MetricsPage } from "pages/admin/MetricsPage"
import { AddPage } from "pages/app/AddPage"
import { CategoryDetailsPage } from "pages/app/CategoryDetailsPage"
import { FeedDetailsPage } from "pages/app/FeedDetailsPage"
@@ -78,6 +79,7 @@ function AppRoutes() {
} />
} />
+ } />
} />
diff --git a/commafeed-client/src/app/client.ts b/commafeed-client/src/app/client.ts
index 9d766638..0504b026 100644
--- a/commafeed-client/src/app/client.ts
+++ b/commafeed-client/src/app/client.ts
@@ -12,6 +12,7 @@ import {
IDRequest,
LoginRequest,
MarkRequest,
+ Metrics,
PasswordResetRequest,
ProfileModificationRequest,
RegistrationRequest,
@@ -79,6 +80,7 @@ export const client = {
getAllUsers: () => axiosInstance.get("admin/user/getAll"),
saveUser: (req: UserModel) => axiosInstance.post("admin/user/save", req),
deleteUser: (req: IDRequest) => axiosInstance.post("admin/user/delete", req),
+ getMetrics: () => axiosInstance.get("admin/metrics"),
},
}
diff --git a/commafeed-client/src/app/slices/redirect.ts b/commafeed-client/src/app/slices/redirect.ts
index fc41da38..c138d470 100644
--- a/commafeed-client/src/app/slices/redirect.ts
+++ b/commafeed-client/src/app/slices/redirect.ts
@@ -37,6 +37,9 @@ export const redirectToSettings = createAsyncThunk("redirect/settings", (_, thun
export const redirectToAdminUsers = createAsyncThunk("redirect/admin/users", (_, thunkApi) =>
thunkApi.dispatch(redirectTo("/app/admin/users"))
)
+export const redirectToMetrics = createAsyncThunk("redirect/admin/metrics", (_, thunkApi) =>
+ thunkApi.dispatch(redirectTo("/app/admin/metrics"))
+)
export const redirectSlice = createSlice({
name: "redirect",
diff --git a/commafeed-client/src/app/types.ts b/commafeed-client/src/app/types.ts
index 17dcc5c0..6683b4ce 100644
--- a/commafeed-client/src/app/types.ts
+++ b/commafeed-client/src/app/types.ts
@@ -115,6 +115,22 @@ export interface FeedModificationRequest {
filter?: string
}
+export interface GetEntriesRequest {
+ id: string
+ readType?: ReadingMode
+ newerThan?: number
+ order?: ReadingOrder
+ keywords?: string
+ onlyIds?: boolean
+ excludedSubscriptionIds?: string
+ tag?: string
+}
+
+export interface GetEntriesPaginatedRequest extends GetEntriesRequest {
+ offset: number
+ limit: number
+}
+
export interface IDRequest {
id: number
}
@@ -132,6 +148,50 @@ export interface MarkRequest {
excludedSubscriptions?: number[]
}
+export interface MetricCounter {
+ count: number
+}
+
+export interface MetricGauge {
+ value: number
+}
+
+export interface MetricMeter {
+ count: number
+ m15_rate: number
+ m1_rate: number
+ m5_rate: number
+ mean_rate: number
+ units: string
+}
+
+export type MetricTimer = {
+ count: number
+ max: number
+ mean: number
+ min: number
+ p50: number
+ p75: number
+ p95: number
+ p98: number
+ p99: number
+ p999: number
+ stddev: number
+ m15_rate: number
+ m1_rate: number
+ m5_rate: number
+ mean_rate: number
+ duration_units: string
+ rate_units: string
+}
+
+export interface Metrics {
+ counters: { [key: string]: MetricCounter }
+ gauges: { [key: string]: MetricGauge }
+ meters: { [key: string]: MetricMeter }
+ timers: { [key: string]: MetricTimer }
+}
+
export interface MultipleMarkRequest {
requests: MarkRequest[]
}
@@ -219,22 +279,6 @@ export interface TagRequest {
tags: string[]
}
-export interface GetEntriesRequest {
- id: string
- readType?: ReadingMode
- newerThan?: number
- order?: ReadingOrder
- keywords?: string
- onlyIds?: boolean
- excludedSubscriptionIds?: string
- tag?: string
-}
-
-export interface GetEntriesPaginatedRequest extends GetEntriesRequest {
- offset: number
- limit: number
-}
-
export interface UnreadCount {
feedId?: number
unreadCount?: number
diff --git a/commafeed-client/src/components/header/ProfileMenu.tsx b/commafeed-client/src/components/header/ProfileMenu.tsx
index 897583f3..4c8f088a 100644
--- a/commafeed-client/src/components/header/ProfileMenu.tsx
+++ b/commafeed-client/src/components/header/ProfileMenu.tsx
@@ -1,9 +1,9 @@
import { Trans } from "@lingui/macro"
import { Divider, Menu, useMantineColorScheme } from "@mantine/core"
-import { redirectToAdminUsers, redirectToSettings } from "app/slices/redirect"
+import { redirectToAdminUsers, redirectToMetrics, redirectToSettings } from "app/slices/redirect"
import { useAppDispatch, useAppSelector } from "app/store"
import { useState } from "react"
-import { TbMoon, TbPower, TbSettings, TbSun, TbUsers } from "react-icons/tb"
+import { TbChartLine, TbMoon, TbPower, TbSettings, TbSun, TbUsers } from "react-icons/tb"
interface ProfileMenuProps {
control: React.ReactElement
@@ -52,6 +52,15 @@ export function ProfileMenu(props: ProfileMenuProps) {
>
Manage users
+ }
+ onClick={() => {
+ dispatch(redirectToMetrics())
+ setOpened(false)
+ }}
+ >
+ Metrics
+
>
)}
diff --git a/commafeed-client/src/components/metrics/Gauge.tsx b/commafeed-client/src/components/metrics/Gauge.tsx
new file mode 100644
index 00000000..26fe9b83
--- /dev/null
+++ b/commafeed-client/src/components/metrics/Gauge.tsx
@@ -0,0 +1,9 @@
+import { MetricGauge } from "app/types"
+
+interface MeterProps {
+ gauge: MetricGauge
+}
+
+export function Gauge(props: MeterProps) {
+ return {props.gauge.value}
+}
diff --git a/commafeed-client/src/components/metrics/Meter.tsx b/commafeed-client/src/components/metrics/Meter.tsx
new file mode 100644
index 00000000..d8dc1978
--- /dev/null
+++ b/commafeed-client/src/components/metrics/Meter.tsx
@@ -0,0 +1,19 @@
+import { Box } from "@mantine/core"
+import { MetricMeter } from "app/types"
+
+interface MeterProps {
+ meter: MetricMeter
+}
+
+export function Meter(props: MeterProps) {
+ return (
+
+ Mean: {props.meter.mean_rate.toFixed(2)}
+ Last minute: {props.meter.m1_rate.toFixed(2)}
+ Last 5 minutes: {props.meter.m5_rate.toFixed(2)}
+ Last 15 minutes: {props.meter.m15_rate.toFixed(2)}
+ Units: {props.meter.units}
+ Total: {props.meter.count}
+
+ )
+}
diff --git a/commafeed-client/src/components/metrics/MetricAccordionItem.tsx b/commafeed-client/src/components/metrics/MetricAccordionItem.tsx
new file mode 100644
index 00000000..658f3f29
--- /dev/null
+++ b/commafeed-client/src/components/metrics/MetricAccordionItem.tsx
@@ -0,0 +1,22 @@
+import { Accordion, Box, Group } from "@mantine/core"
+
+interface MetricAccordionItemProps {
+ metricKey: string
+ name: string
+ headerValue: number
+ children: React.ReactNode
+}
+
+export function MetricAccordionItem({ metricKey, name, headerValue, children }: MetricAccordionItemProps) {
+ return (
+
+
+
+ {name}
+ {headerValue}
+
+
+ {children}
+
+ )
+}
diff --git a/commafeed-client/src/components/metrics/Timer.tsx b/commafeed-client/src/components/metrics/Timer.tsx
new file mode 100644
index 00000000..2ce8b3d0
--- /dev/null
+++ b/commafeed-client/src/components/metrics/Timer.tsx
@@ -0,0 +1,19 @@
+import { Box } from "@mantine/core"
+import { MetricTimer } from "app/types"
+
+interface MetricTimerProps {
+ timer: MetricTimer
+}
+
+export function Timer(props: MetricTimerProps) {
+ return (
+
+ Mean: {props.timer.mean_rate.toFixed(2)}
+ Last minute: {props.timer.m1_rate.toFixed(2)}
+ Last 5 minutes: {props.timer.m5_rate.toFixed(2)}
+ Last 15 minutes: {props.timer.m15_rate.toFixed(2)}
+ Units: {props.timer.rate_units}
+ Total: {props.timer.count}
+
+ )
+}
diff --git a/commafeed-client/src/pages/admin/MetricsPage.tsx b/commafeed-client/src/pages/admin/MetricsPage.tsx
new file mode 100644
index 00000000..15b024d2
--- /dev/null
+++ b/commafeed-client/src/pages/admin/MetricsPage.tsx
@@ -0,0 +1,75 @@
+import { Accordion, Box, Tabs } from "@mantine/core"
+import { client } from "app/client"
+import { Loader } from "components/Loader"
+import { Gauge } from "components/metrics/Gauge"
+import { Meter } from "components/metrics/Meter"
+import { MetricAccordionItem } from "components/metrics/MetricAccordionItem"
+import { Timer } from "components/metrics/Timer"
+import { useAsync } from "react-async-hook"
+import { TbChartAreaLine, TbClock } from "react-icons/tb"
+
+const shownMeters: { [key: string]: string } = {
+ "com.commafeed.backend.feed.FeedQueues.refill": "Refresh queue refill rate",
+ "com.commafeed.backend.feed.FeedRefreshTaskGiver.feedRefreshed": "Feed refreshed",
+ "com.commafeed.backend.feed.FeedRefreshUpdater.feedUpdated": "Feed updated",
+ "com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheHit": "Entry cache hit",
+ "com.commafeed.backend.feed.FeedRefreshUpdater.entryCacheMiss": "Entry cache miss",
+}
+
+const shownGauges: { [key: string]: string } = {
+ "com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.active": "Feed Updater active",
+ "com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-updater.pending": "Feed Updater queued",
+ "com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.active": "Feed Worker active",
+ "com.commafeed.backend.feed.FeedRefreshExecutor.feed-refresh-worker.pending": "Feed Worker queued",
+ "com.commafeed.backend.feed.FeedQueues.addQueue": "Task Giver Add Queue",
+ "com.commafeed.backend.feed.FeedQueues.takeQueue": "Task Giver Take Queue",
+ "com.commafeed.backend.feed.FeedQueues.giveBackQueue": "Task Giver Give Back Queue",
+}
+
+export function MetricsPage() {
+ const query = useAsync(() => client.admin.getMetrics(), [])
+
+ if (!query.result) return
+ const { meters, gauges, timers } = query.result.data
+ return (
+
+
+ }>
+ Stats
+
+ }>
+ Timers
+
+
+
+
+
+ {Object.keys(shownMeters).map(m => (
+
+
+
+ ))}
+
+
+
+ {Object.keys(shownGauges).map(g => (
+
+ {shownGauges[g]}
+
+
+ ))}
+
+
+
+
+
+ {Object.keys(timers).map(key => (
+
+
+
+ ))}
+
+
+
+ )
+}