Add monitoring on queries with sentry alert + Fix check position list in db for backtest
This commit is contained in:
@@ -48,7 +48,7 @@ const LogIn = () => {
|
||||
.user_CreateToken({
|
||||
address: walletAddress,
|
||||
message: message,
|
||||
name: form.name,
|
||||
name: user?.id,
|
||||
signature: signature,
|
||||
})
|
||||
.then((data) => {
|
||||
@@ -101,19 +101,6 @@ const LogIn = () => {
|
||||
action="#"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="dark:text-white block mb-2 text-sm font-medium text-gray-900"
|
||||
hidden={true}
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
{...register('name')}
|
||||
></input>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn bg-primary w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function Table({
|
||||
) as TableInstanceWithHooks<any>
|
||||
|
||||
// Calculez le total des valeurs dans la colonne USD
|
||||
const total = data
|
||||
const total = data && showTotal
|
||||
? data
|
||||
.reduce((sum: number, row: any) => {
|
||||
return sum + (row.value || 0) // Si la valeur est undefined = 0
|
||||
|
||||
@@ -3062,6 +3062,224 @@ export class SettingsClient extends AuthorizedApiBase {
|
||||
}
|
||||
}
|
||||
|
||||
export class SqlMonitoringClient extends AuthorizedApiBase {
|
||||
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||
private baseUrl: string;
|
||||
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
|
||||
|
||||
constructor(configuration: IConfig, baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
|
||||
super(configuration);
|
||||
this.http = http ? http : window as any;
|
||||
this.baseUrl = baseUrl ?? "http://localhost:5000";
|
||||
}
|
||||
|
||||
sqlMonitoring_GetQueryStatistics(): Promise<FileResponse> {
|
||||
let url_ = this.baseUrl + "/api/SqlMonitoring/statistics";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_: RequestInit = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/octet-stream"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processSqlMonitoring_GetQueryStatistics(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processSqlMonitoring_GetQueryStatistics(response: Response): Promise<FileResponse> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200 || status === 206) {
|
||||
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
|
||||
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
|
||||
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
|
||||
if (fileName) {
|
||||
fileName = decodeURIComponent(fileName);
|
||||
} else {
|
||||
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
|
||||
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
|
||||
}
|
||||
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<FileResponse>(null as any);
|
||||
}
|
||||
|
||||
sqlMonitoring_GetAlerts(): Promise<FileResponse> {
|
||||
let url_ = this.baseUrl + "/api/SqlMonitoring/alerts";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_: RequestInit = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/octet-stream"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processSqlMonitoring_GetAlerts(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processSqlMonitoring_GetAlerts(response: Response): Promise<FileResponse> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200 || status === 206) {
|
||||
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
|
||||
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
|
||||
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
|
||||
if (fileName) {
|
||||
fileName = decodeURIComponent(fileName);
|
||||
} else {
|
||||
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
|
||||
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
|
||||
}
|
||||
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<FileResponse>(null as any);
|
||||
}
|
||||
|
||||
sqlMonitoring_ClearTracking(): Promise<FileResponse> {
|
||||
let url_ = this.baseUrl + "/api/SqlMonitoring/clear-tracking";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_: RequestInit = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/octet-stream"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processSqlMonitoring_ClearTracking(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processSqlMonitoring_ClearTracking(response: Response): Promise<FileResponse> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200 || status === 206) {
|
||||
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
|
||||
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
|
||||
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
|
||||
if (fileName) {
|
||||
fileName = decodeURIComponent(fileName);
|
||||
} else {
|
||||
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
|
||||
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
|
||||
}
|
||||
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<FileResponse>(null as any);
|
||||
}
|
||||
|
||||
sqlMonitoring_GetQueryDetails(repositoryName: string, methodName: string): Promise<FileResponse> {
|
||||
let url_ = this.baseUrl + "/api/SqlMonitoring/query-details/{repositoryName}/{methodName}";
|
||||
if (repositoryName === undefined || repositoryName === null)
|
||||
throw new Error("The parameter 'repositoryName' must be defined.");
|
||||
url_ = url_.replace("{repositoryName}", encodeURIComponent("" + repositoryName));
|
||||
if (methodName === undefined || methodName === null)
|
||||
throw new Error("The parameter 'methodName' must be defined.");
|
||||
url_ = url_.replace("{methodName}", encodeURIComponent("" + methodName));
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_: RequestInit = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/octet-stream"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processSqlMonitoring_GetQueryDetails(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processSqlMonitoring_GetQueryDetails(response: Response): Promise<FileResponse> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200 || status === 206) {
|
||||
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
|
||||
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
|
||||
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
|
||||
if (fileName) {
|
||||
fileName = decodeURIComponent(fileName);
|
||||
} else {
|
||||
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
|
||||
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
|
||||
}
|
||||
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<FileResponse>(null as any);
|
||||
}
|
||||
|
||||
sqlMonitoring_GetMonitoringHealth(): Promise<FileResponse> {
|
||||
let url_ = this.baseUrl + "/api/SqlMonitoring/health";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_: RequestInit = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/octet-stream"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processSqlMonitoring_GetMonitoringHealth(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processSqlMonitoring_GetMonitoringHealth(response: Response): Promise<FileResponse> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200 || status === 206) {
|
||||
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
|
||||
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
|
||||
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
|
||||
if (fileName) {
|
||||
fileName = decodeURIComponent(fileName);
|
||||
} else {
|
||||
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
|
||||
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
|
||||
}
|
||||
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<FileResponse>(null as any);
|
||||
}
|
||||
}
|
||||
|
||||
export class TradingClient extends AuthorizedApiBase {
|
||||
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||
private baseUrl: string;
|
||||
|
||||
182
src/Managing.WebApp/src/hooks/useSqlMonitoring.tsx
Normal file
182
src/Managing.WebApp/src/hooks/useSqlMonitoring.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import useApiUrlStore from '../app/store/apiStore'
|
||||
import {SqlMonitoringClient} from '../generated/ManagingApi'
|
||||
|
||||
// Interface for SQL monitoring statistics
|
||||
interface SqlMonitoringStats {
|
||||
loopDetectionStats: Record<string, any>
|
||||
contextStats: Record<string, any>
|
||||
timestamp: string
|
||||
totalTrackedQueries: number
|
||||
activeQueries: number
|
||||
}
|
||||
|
||||
// Interface for SQL monitoring alerts
|
||||
interface SqlMonitoringAlert {
|
||||
id: string
|
||||
type: string
|
||||
message: string
|
||||
timestamp: string
|
||||
repository: string
|
||||
method: string
|
||||
severity: string
|
||||
}
|
||||
|
||||
// Interface for monitoring health
|
||||
interface MonitoringHealth {
|
||||
isEnabled: boolean
|
||||
loggingEnabled: boolean
|
||||
sentryEnabled: boolean
|
||||
loopDetectionEnabled: boolean
|
||||
performanceMonitoringEnabled: boolean
|
||||
lastHealthCheck: string
|
||||
totalAlerts: number
|
||||
activeQueries: number
|
||||
}
|
||||
|
||||
// Interface for query details
|
||||
interface QueryDetail {
|
||||
repository: string
|
||||
method: string
|
||||
queryPattern: string
|
||||
executionCount: number
|
||||
averageExecutionTime: number
|
||||
lastExecution: string
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
// Hook for SQL monitoring statistics
|
||||
export const useSqlMonitoringStats = () => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const sqlMonitoringClient = new SqlMonitoringClient({}, apiUrl)
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['sqlMonitoring', 'statistics'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await sqlMonitoringClient.sqlMonitoring_GetQueryStatistics()
|
||||
const text = await response.data.text()
|
||||
const data = JSON.parse(text) as SqlMonitoringStats
|
||||
|
||||
// Ensure the data has the expected structure
|
||||
return {
|
||||
loopDetectionStats: data.loopDetectionStats || {},
|
||||
contextStats: data.contextStats || {},
|
||||
timestamp: data.timestamp || new Date().toISOString(),
|
||||
totalTrackedQueries: data.totalTrackedQueries || 0,
|
||||
activeQueries: data.activeQueries || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching SQL monitoring statistics:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
refetchInterval: 30000, // Refresh every 30 seconds
|
||||
})
|
||||
}
|
||||
|
||||
// Hook for SQL monitoring alerts
|
||||
export const useSqlMonitoringAlerts = () => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const sqlMonitoringClient = new SqlMonitoringClient({}, apiUrl)
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['sqlMonitoring', 'alerts'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await sqlMonitoringClient.sqlMonitoring_GetAlerts()
|
||||
const text = await response.data.text()
|
||||
const data = JSON.parse(text) as SqlMonitoringAlert[]
|
||||
|
||||
// Ensure we return an array
|
||||
return Array.isArray(data) ? data : []
|
||||
} catch (error) {
|
||||
console.error('Error fetching SQL monitoring alerts:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
refetchInterval: 15000, // Refresh every 15 seconds
|
||||
})
|
||||
}
|
||||
|
||||
// Hook for monitoring health
|
||||
export const useSqlMonitoringHealth = () => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const sqlMonitoringClient = new SqlMonitoringClient({}, apiUrl)
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['sqlMonitoring', 'health'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await sqlMonitoringClient.sqlMonitoring_GetMonitoringHealth()
|
||||
const text = await response.data.text()
|
||||
const data = JSON.parse(text) as MonitoringHealth
|
||||
|
||||
// Ensure the data has the expected structure
|
||||
return {
|
||||
isEnabled: data.isEnabled || false,
|
||||
loggingEnabled: data.loggingEnabled || false,
|
||||
sentryEnabled: data.sentryEnabled || false,
|
||||
loopDetectionEnabled: data.loopDetectionEnabled || false,
|
||||
performanceMonitoringEnabled: data.performanceMonitoringEnabled || false,
|
||||
lastHealthCheck: data.lastHealthCheck || new Date().toISOString(),
|
||||
totalAlerts: data.totalAlerts || 0,
|
||||
activeQueries: data.activeQueries || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching SQL monitoring health:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
refetchInterval: 60000, // Refresh every minute
|
||||
})
|
||||
}
|
||||
|
||||
// Hook for query details
|
||||
export const useSqlMonitoringQueryDetails = (repositoryName: string, methodName: string) => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const sqlMonitoringClient = new SqlMonitoringClient({}, apiUrl)
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['sqlMonitoring', 'queryDetails', repositoryName, methodName],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await sqlMonitoringClient.sqlMonitoring_GetQueryDetails(repositoryName, methodName)
|
||||
const text = await response.data.text()
|
||||
const data = JSON.parse(text) as QueryDetail[]
|
||||
|
||||
// Ensure we return an array
|
||||
return Array.isArray(data) ? data : []
|
||||
} catch (error) {
|
||||
console.error('Error fetching SQL monitoring query details:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
enabled: !!repositoryName && !!methodName, // Only run if both parameters are provided
|
||||
})
|
||||
}
|
||||
|
||||
// Hook for clearing tracking data
|
||||
export const useClearSqlMonitoringTracking = () => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const queryClient = useQueryClient()
|
||||
const sqlMonitoringClient = new SqlMonitoringClient({}, apiUrl)
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
await sqlMonitoringClient.sqlMonitoring_ClearTracking()
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate all SQL monitoring queries to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: ['sqlMonitoring'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Export types for use in components
|
||||
export type {
|
||||
SqlMonitoringStats,
|
||||
SqlMonitoringAlert,
|
||||
MonitoringHealth,
|
||||
QueryDetail,
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import Theme from './theme'
|
||||
import DefaultConfig from './defaultConfig/defaultConfig'
|
||||
import UserInfoSettings from './UserInfoSettings'
|
||||
import AccountFee from './accountFee/accountFee'
|
||||
import SqlMonitoring from './sqlmonitoring/sqlMonitoring'
|
||||
|
||||
type TabsType = {
|
||||
label: string
|
||||
@@ -53,6 +54,11 @@ const tabs: TabsType = [
|
||||
index: 7,
|
||||
label: 'Health Checks',
|
||||
},
|
||||
{
|
||||
Component: SqlMonitoring,
|
||||
index: 8,
|
||||
label: 'SQL Monitoring',
|
||||
},
|
||||
]
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# SQL Monitoring Dashboard
|
||||
|
||||
This component provides a comprehensive single-page dashboard for monitoring SQL query performance, loop detection, and system health in the Managing application.
|
||||
|
||||
## Features
|
||||
|
||||
### Overview Cards
|
||||
- **Total Tracked Queries**: Shows the total number of queries tracked by the monitoring system
|
||||
- **Active Queries**: Displays currently monitored queries
|
||||
- **Total Alerts**: Shows the number of alerts generated by the system
|
||||
- **Monitoring Status**: Indicates whether SQL monitoring is active or inactive
|
||||
|
||||
### System Health Section
|
||||
- **Monitoring Status**: Overall health of the SQL monitoring system
|
||||
- **Feature Status**: Individual status of monitoring features (logging, Sentry, loop detection, etc.)
|
||||
- **Compact Layout**: All health indicators displayed in a responsive grid
|
||||
|
||||
### Recent Alerts Section
|
||||
- **SQL Monitoring Alerts**: Real-time alerts for SQL performance issues
|
||||
- **Severity Levels**: Critical, Warning, and Info alerts with color-coded badges
|
||||
- **Repository and Method**: Shows which repository and method triggered the alert
|
||||
- **Timestamp**: When the alert was generated
|
||||
|
||||
### Query Statistics Section
|
||||
- **Query Statistics**: Detailed statistics about query execution patterns
|
||||
- **Loop Detection Stats**: Information about detected query loops
|
||||
- **Context Stats**: Additional context information about the monitoring system
|
||||
|
||||
### Information Panel
|
||||
- **Usage Instructions**: Explains how the dashboard works
|
||||
- **Auto-refresh Info**: Details about automatic data updates
|
||||
- **Clear Data Instructions**: How to reset monitoring statistics
|
||||
|
||||
## API Integration
|
||||
|
||||
The component integrates with the following SQL monitoring endpoints:
|
||||
|
||||
- `GET /api/sqlmonitoring/statistics` - Get query statistics
|
||||
- `GET /api/sqlmonitoring/alerts` - Get monitoring alerts
|
||||
- `GET /api/sqlmonitoring/health` - Get monitoring health status
|
||||
- `POST /api/sqlmonitoring/clear` - Clear tracking data
|
||||
- `GET /api/sqlmonitoring/details/{repositoryName}/{methodName}` - Get query details
|
||||
|
||||
## Auto-refresh
|
||||
|
||||
- **Statistics**: Refreshes every 30 seconds
|
||||
- **Alerts**: Refreshes every 15 seconds
|
||||
- **Health**: Refreshes every minute
|
||||
|
||||
## Admin Authorization
|
||||
|
||||
All SQL monitoring endpoints require admin authorization. Only users with admin privileges can access this dashboard.
|
||||
|
||||
## Mobile-Friendly Design
|
||||
|
||||
The dashboard is designed to be fully responsive and mobile-friendly:
|
||||
|
||||
- **Responsive Grid**: Overview cards adapt from 1 column on mobile to 4 columns on desktop
|
||||
- **Compact Layout**: Health status indicators are arranged in a responsive grid
|
||||
- **Horizontal Scrolling**: Tables have horizontal scroll on smaller screens
|
||||
- **Touch-Friendly**: All interactive elements are appropriately sized for touch devices
|
||||
- **Readable Text**: Font sizes and spacing optimized for mobile viewing
|
||||
|
||||
## Usage
|
||||
|
||||
1. Navigate to Settings → SQL Monitoring
|
||||
2. View all monitoring information on a single page
|
||||
3. Use the "Clear Tracking Data" button to reset monitoring statistics
|
||||
4. Monitor alerts and statistics in real-time
|
||||
5. Check system health status at a glance
|
||||
|
||||
## Configuration
|
||||
|
||||
The SQL monitoring system can be configured via `appsettings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"SqlMonitoring": {
|
||||
"Enabled": true,
|
||||
"LoggingEnabled": true,
|
||||
"SentryEnabled": true,
|
||||
"LoopDetectionEnabled": true,
|
||||
"PerformanceMonitoringEnabled": true,
|
||||
"LoopDetectionWindowSeconds": 60,
|
||||
"MaxQueryExecutionsPerWindow": 100,
|
||||
"MaxMethodExecutionsPerWindow": 50,
|
||||
"LongRunningQueryThresholdMs": 1000,
|
||||
"SentryAlertThreshold": 5,
|
||||
"SlowQueryThresholdMs": 2000,
|
||||
"LogSlowQueriesOnly": false,
|
||||
"LogErrorsOnly": false
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,311 @@
|
||||
import React from 'react'
|
||||
import {Table} from '../../../components/mollecules'
|
||||
import {
|
||||
useClearSqlMonitoringTracking,
|
||||
useSqlMonitoringAlerts,
|
||||
useSqlMonitoringHealth,
|
||||
useSqlMonitoringStats,
|
||||
} from '../../../hooks/useSqlMonitoring'
|
||||
|
||||
const SqlMonitoring: React.FC = () => {
|
||||
// Use custom hooks for SQL monitoring data
|
||||
const { data: statistics, isLoading: isLoadingStats } = useSqlMonitoringStats()
|
||||
const { data: alerts, isLoading: isLoadingAlerts } = useSqlMonitoringAlerts()
|
||||
const { data: health, isLoading: isLoadingHealth } = useSqlMonitoringHealth()
|
||||
const clearTrackingMutation = useClearSqlMonitoringTracking()
|
||||
|
||||
const isLoading = isLoadingStats || isLoadingAlerts || isLoadingHealth
|
||||
|
||||
// Prepare statistics data for table
|
||||
const statisticsData = React.useMemo(() => {
|
||||
if (!statistics) return []
|
||||
|
||||
const stats: Array<{
|
||||
type: string
|
||||
key: string
|
||||
value: string
|
||||
timestamp: string
|
||||
}> = []
|
||||
|
||||
// Add loop detection stats
|
||||
if (statistics.loopDetectionStats) {
|
||||
Object.entries(statistics.loopDetectionStats).forEach(([key, value]) => {
|
||||
stats.push({
|
||||
type: 'Loop Detection',
|
||||
key,
|
||||
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
|
||||
timestamp: statistics.timestamp,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Add context stats
|
||||
if (statistics.contextStats) {
|
||||
Object.entries(statistics.contextStats).forEach(([key, value]) => {
|
||||
stats.push({
|
||||
type: 'Context',
|
||||
key,
|
||||
value: String(value),
|
||||
timestamp: statistics.timestamp,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return stats
|
||||
}, [statistics])
|
||||
|
||||
// Prepare alerts data for table
|
||||
const alertsData = React.useMemo(() => {
|
||||
if (!alerts || !Array.isArray(alerts)) return []
|
||||
return alerts.map(alert => ({
|
||||
id: alert.id || 'unknown',
|
||||
type: alert.type || 'unknown',
|
||||
message: alert.message || 'No message',
|
||||
timestamp: alert.timestamp || new Date().toISOString(),
|
||||
repository: alert.repository || 'unknown',
|
||||
method: alert.method || 'unknown',
|
||||
severity: alert.severity || 'info',
|
||||
}))
|
||||
}, [alerts])
|
||||
|
||||
// Define columns for statistics table
|
||||
const statisticsColumns = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Type',
|
||||
accessor: 'type',
|
||||
disableSortBy: true,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: 'Key',
|
||||
accessor: 'key',
|
||||
disableSortBy: true,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: 'Value',
|
||||
accessor: 'value',
|
||||
disableSortBy: true,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: 'Timestamp',
|
||||
accessor: 'timestamp',
|
||||
disableSortBy: true,
|
||||
disableFilters: true,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
// Define columns for alerts table
|
||||
const alertsColumns = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Severity',
|
||||
accessor: 'severity',
|
||||
Cell: ({ value }: { value: string }) => (
|
||||
<span
|
||||
className={`badge ${
|
||||
value === 'Critical' || value === 'Error'
|
||||
? 'badge-error'
|
||||
: value === 'Warning'
|
||||
? 'badge-warning'
|
||||
: 'badge-info'
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
),
|
||||
disableSortBy: true,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: 'Type',
|
||||
accessor: 'type',
|
||||
disableSortBy: true,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: 'Repository',
|
||||
accessor: 'repository',
|
||||
disableSortBy: true,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: 'Method',
|
||||
accessor: 'method',
|
||||
disableSortBy: true,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: 'Message',
|
||||
accessor: 'message',
|
||||
disableSortBy: true,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: 'Timestamp',
|
||||
accessor: 'timestamp',
|
||||
disableSortBy: true,
|
||||
disableFilters: true,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<h2 className="text-2xl font-bold">SQL Monitoring Dashboard</h2>
|
||||
<button
|
||||
className="btn btn-outline btn-sm"
|
||||
onClick={() => clearTrackingMutation.mutate()}
|
||||
disabled={clearTrackingMutation.isPending}
|
||||
>
|
||||
{clearTrackingMutation.isPending ? 'Clearing...' : 'Clear Tracking Data'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center">
|
||||
<progress className="progress progress-primary w-56"></progress>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Overview Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="stat bg-base-200 rounded-lg">
|
||||
<div className="stat-title text-sm">Total Tracked Queries</div>
|
||||
<div className="stat-value text-lg text-primary">
|
||||
{statistics?.totalTrackedQueries || 0}
|
||||
</div>
|
||||
<div className="stat-desc text-xs">All time</div>
|
||||
</div>
|
||||
<div className="stat bg-base-200 rounded-lg">
|
||||
<div className="stat-title text-sm">Active Queries</div>
|
||||
<div className="stat-value text-lg text-secondary">
|
||||
{statistics?.activeQueries || 0}
|
||||
</div>
|
||||
<div className="stat-desc text-xs">Currently monitored</div>
|
||||
</div>
|
||||
<div className="stat bg-base-200 rounded-lg">
|
||||
<div className="stat-title text-sm">Total Alerts</div>
|
||||
<div className="stat-value text-lg text-warning">
|
||||
{alerts?.length || 0}
|
||||
</div>
|
||||
<div className="stat-desc text-xs">All alerts</div>
|
||||
</div>
|
||||
<div className="stat bg-base-200 rounded-lg">
|
||||
<div className="stat-title text-sm">Monitoring Status</div>
|
||||
<div className={`stat-value text-lg ${health?.isEnabled ? 'text-success' : 'text-error'}`}>
|
||||
{health?.isEnabled ? 'Active' : 'Inactive'}
|
||||
</div>
|
||||
<div className="stat-desc text-xs">System status</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Health Status */}
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-lg">System Health</h3>
|
||||
{health ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-sm font-medium mb-1">Monitoring</span>
|
||||
<span className={`badge ${health.isEnabled ? 'badge-success' : 'badge-error'}`}>
|
||||
{health.isEnabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-sm font-medium mb-1">Logging</span>
|
||||
<span className={`badge ${health.loggingEnabled ? 'badge-success' : 'badge-error'}`}>
|
||||
{health.loggingEnabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-sm font-medium mb-1">Sentry</span>
|
||||
<span className={`badge ${health.sentryEnabled ? 'badge-success' : 'badge-error'}`}>
|
||||
{health.sentryEnabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-sm font-medium mb-1">Loop Detection</span>
|
||||
<span className={`badge ${health.loopDetectionEnabled ? 'badge-success' : 'badge-error'}`}>
|
||||
{health.loopDetectionEnabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-sm font-medium mb-1">Performance</span>
|
||||
<span className={`badge ${health.performanceMonitoringEnabled ? 'badge-success' : 'badge-error'}`}>
|
||||
{health.performanceMonitoringEnabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Unable to fetch monitoring health data</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alerts Section */}
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-lg">Recent Alerts</h3>
|
||||
{alertsData.length === 0 ? (
|
||||
<div className="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current shrink-0 w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>No alerts found. The system is running smoothly!</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
columns={alertsColumns}
|
||||
data={alertsData}
|
||||
showPagination={true}
|
||||
showTotal={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics Section */}
|
||||
<div className="card bg-base-200">
|
||||
<div className="card-body">
|
||||
<h3 className="card-title text-lg">Query Statistics</h3>
|
||||
{statisticsData.length === 0 ? (
|
||||
<div className="alert alert-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
<span>No statistics available yet</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
columns={statisticsColumns}
|
||||
data={statisticsData}
|
||||
showPagination={true}
|
||||
showTotal={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SqlMonitoring
|
||||
Reference in New Issue
Block a user