Skip to content

Commit 45814f5

Browse files
committed
feat: add API logs section to management; implement TelemetryLogDetails component and GraphQL queries
1 parent 841b4bc commit 45814f5

File tree

7 files changed

+400
-2
lines changed

7 files changed

+400
-2
lines changed

backend/src/interceptor/LoggingInterceptor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Observable } from 'rxjs';
1010
import { tap } from 'rxjs/operators';
1111
import { GqlExecutionContext } from '@nestjs/graphql';
1212
import { TelemetryLogService } from './telemetry-log.service';
13+
import { GetUserIdFromToken } from 'src/decorator/get-auth-token.decorator';
1314

1415
@Injectable()
1516
export class LoggingInterceptor implements NestInterceptor {
@@ -38,6 +39,7 @@ export class LoggingInterceptor implements NestInterceptor {
3839
): Observable<any> {
3940
const ctx = GqlExecutionContext.create(context);
4041
const info = ctx.getInfo();
42+
const userId = ctx.getContext().req.user?.userId;
4143
if (!info) {
4244
this.logger.warn(
4345
'GraphQL request detected, but ctx.getInfo() is undefined.',
@@ -49,7 +51,6 @@ export class LoggingInterceptor implements NestInterceptor {
4951
let variables = '';
5052
const startTime = Date.now();
5153
const request = ctx.getContext().req;
52-
const userId = request?.user?.id;
5354

5455
try {
5556
variables = JSON.stringify(ctx.getContext()?.req?.body?.variables ?? {});
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React from 'react';
2+
import {
3+
Dialog,
4+
DialogTitle,
5+
DialogContent,
6+
DialogActions,
7+
Button,
8+
Typography,
9+
Box,
10+
Paper,
11+
CircularProgress
12+
} from '@mui/material';
13+
import { format } from 'date-fns';
14+
import { useQuery } from '@apollo/client';
15+
import { GET_DASHBOARD_TELEMETRY_LOG } from 'src/graphql/request';
16+
17+
interface TelemetryLogDetailsProps {
18+
open: boolean;
19+
onClose: () => void;
20+
logId: string | null;
21+
}
22+
23+
const DataSection = ({
24+
title,
25+
content
26+
}: {
27+
title: string;
28+
content: string;
29+
}) => (
30+
<Box mb={2}>
31+
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
32+
{title}
33+
</Typography>
34+
<Paper
35+
variant="outlined"
36+
sx={{ p: 2, maxHeight: '200px', overflow: 'auto' }}
37+
>
38+
<pre
39+
style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
40+
>
41+
{content}
42+
</pre>
43+
</Paper>
44+
</Box>
45+
);
46+
47+
const TelemetryLogDetails: React.FC<TelemetryLogDetailsProps> = ({
48+
open,
49+
onClose,
50+
logId
51+
}) => {
52+
const { data, loading } = useQuery(GET_DASHBOARD_TELEMETRY_LOG, {
53+
variables: { id: logId },
54+
skip: !logId
55+
});
56+
57+
const log = data?.dashboardTelemetryLog;
58+
59+
if (!open) return null;
60+
61+
return (
62+
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
63+
<DialogTitle>Telemetry Log Details</DialogTitle>
64+
<DialogContent>
65+
{loading ? (
66+
<Box display="flex" justifyContent="center" p={3}>
67+
<CircularProgress />
68+
</Box>
69+
) : log ? (
70+
<Box mt={2}>
71+
<Box display="flex" gap={4} mb={3}>
72+
<Box>
73+
<Typography variant="caption" color="textSecondary">
74+
Timestamp
75+
</Typography>
76+
<Typography>
77+
{format(new Date(log.timestamp), 'yyyy-MM-dd HH:mm:ss')}
78+
</Typography>
79+
</Box>
80+
<Box>
81+
<Typography variant="caption" color="textSecondary">
82+
Method
83+
</Typography>
84+
<Typography>{log.requestMethod}</Typography>
85+
</Box>
86+
<Box>
87+
<Typography variant="caption" color="textSecondary">
88+
Time Consumed
89+
</Typography>
90+
<Typography>{log.timeConsumed}ms</Typography>
91+
</Box>
92+
</Box>
93+
94+
<DataSection title="Endpoint" content={log.endpoint} />
95+
96+
{log.input && (
97+
<DataSection
98+
title="Request Input"
99+
content={JSON.stringify(JSON.parse(log.input), null, 2)}
100+
/>
101+
)}
102+
103+
{log.output && (
104+
<DataSection
105+
title="Response Output"
106+
content={JSON.stringify(JSON.parse(log.output), null, 2)}
107+
/>
108+
)}
109+
110+
<Box display="flex" gap={4} mt={3}>
111+
<Box>
112+
<Typography variant="caption" color="textSecondary">
113+
User ID
114+
</Typography>
115+
<Typography>{log.userId || '-'}</Typography>
116+
</Box>
117+
<Box>
118+
<Typography variant="caption" color="textSecondary">
119+
Handler
120+
</Typography>
121+
<Typography>{log.handler}</Typography>
122+
</Box>
123+
</Box>
124+
</Box>
125+
) : (
126+
<Typography color="error">Log not found</Typography>
127+
)}
128+
</DialogContent>
129+
<DialogActions>
130+
<Button onClick={onClose}>Close</Button>
131+
</DialogActions>
132+
</Dialog>
133+
);
134+
};
135+
136+
export default TelemetryLogDetails;
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { useState, useCallback } from 'react';
2+
import {
3+
Card,
4+
Table,
5+
TableBody,
6+
TableCell,
7+
TableContainer,
8+
TableHead,
9+
TableRow,
10+
TablePagination,
11+
TextField,
12+
Box,
13+
IconButton,
14+
Tooltip,
15+
Typography
16+
} from '@mui/material';
17+
import { useQuery } from '@apollo/client';
18+
import { format } from 'date-fns';
19+
import VisibilityIcon from '@mui/icons-material/Visibility';
20+
import FilterListIcon from '@mui/icons-material/FilterList';
21+
import TelemetryLogDetails from './TelemetryLogDetails';
22+
import {
23+
GET_DASHBOARD_TELEMETRY_LOGS,
24+
GET_DASHBOARD_TELEMETRY_LOGS_COUNT
25+
} from 'src/graphql/request';
26+
interface TelemetryLog {
27+
id: string;
28+
timestamp: string;
29+
requestMethod: string;
30+
endpoint: string;
31+
input?: string;
32+
output?: string;
33+
timeConsumed: number;
34+
userId?: string;
35+
handler: string;
36+
}
37+
38+
interface TelemetryLogFilterInput {
39+
startDate?: Date;
40+
endDate?: Date;
41+
requestMethod?: string;
42+
endpoint?: string;
43+
userId?: string;
44+
handler?: string;
45+
minTimeConsumed?: number;
46+
maxTimeConsumed?: number;
47+
search?: string;
48+
}
49+
50+
const TelemetryLogs = () => {
51+
const [page, setPage] = useState(0);
52+
const [rowsPerPage] = useState(10);
53+
const [filter, setFilter] = useState<TelemetryLogFilterInput>({});
54+
const [showFilters, setShowFilters] = useState(false);
55+
const [selectedLogId, setSelectedLogId] = useState<string | null>(null);
56+
57+
const { data: logsData, loading: logsLoading } = useQuery(
58+
GET_DASHBOARD_TELEMETRY_LOGS,
59+
{
60+
variables: { filter },
61+
fetchPolicy: 'network-only'
62+
}
63+
);
64+
65+
const { data: countData } = useQuery(GET_DASHBOARD_TELEMETRY_LOGS_COUNT, {
66+
variables: { filter }
67+
});
68+
69+
const handlePageChange = (
70+
event: React.MouseEvent<HTMLButtonElement> | null,
71+
newPage: number
72+
) => {
73+
setPage(newPage);
74+
};
75+
76+
const handleFilterChange =
77+
(field: keyof TelemetryLogFilterInput) =>
78+
(event: React.ChangeEvent<HTMLInputElement>) => {
79+
setFilter((prev) => ({
80+
...prev,
81+
[field]: event.target.value
82+
}));
83+
};
84+
85+
const handleCloseDetails = useCallback(() => {
86+
setSelectedLogId(null);
87+
}, []);
88+
89+
const logs = logsData?.dashboardTelemetryLogs || [];
90+
const totalCount = countData?.dashboardTelemetryLogsCount || 0;
91+
92+
return (
93+
<>
94+
<Card>
95+
<Box
96+
p={2}
97+
display="flex"
98+
justifyContent="space-between"
99+
alignItems="center"
100+
>
101+
<Typography variant="h6">Telemetry Logs</Typography>
102+
<IconButton onClick={() => setShowFilters(!showFilters)}>
103+
<FilterListIcon />
104+
</IconButton>
105+
</Box>
106+
107+
{showFilters && (
108+
<Box p={2} display="flex" gap={2} flexWrap="wrap">
109+
<TextField
110+
label="Method"
111+
size="small"
112+
value={filter.requestMethod || ''}
113+
onChange={handleFilterChange('requestMethod')}
114+
/>
115+
<TextField
116+
label="Endpoint"
117+
size="small"
118+
value={filter.endpoint || ''}
119+
onChange={handleFilterChange('endpoint')}
120+
/>
121+
<TextField
122+
label="Handler"
123+
size="small"
124+
value={filter.handler || ''}
125+
onChange={handleFilterChange('handler')}
126+
/>
127+
<TextField
128+
label="User ID"
129+
size="small"
130+
value={filter.userId || ''}
131+
onChange={handleFilterChange('userId')}
132+
/>
133+
</Box>
134+
)}
135+
136+
<TableContainer>
137+
<Table>
138+
<TableHead>
139+
<TableRow>
140+
<TableCell>Timestamp</TableCell>
141+
<TableCell>Method</TableCell>
142+
<TableCell>Endpoint</TableCell>
143+
<TableCell>Handler</TableCell>
144+
<TableCell>Time (ms)</TableCell>
145+
<TableCell>User ID</TableCell>
146+
<TableCell>Actions</TableCell>
147+
</TableRow>
148+
</TableHead>
149+
<TableBody>
150+
{logsLoading ? (
151+
<TableRow>
152+
<TableCell colSpan={7} align="center">
153+
Loading...
154+
</TableCell>
155+
</TableRow>
156+
) : (
157+
logs.map((log: TelemetryLog) => (
158+
<TableRow key={log.id}>
159+
<TableCell>
160+
{format(new Date(log.timestamp), 'yyyy-MM-dd HH:mm:ss')}
161+
</TableCell>
162+
<TableCell>{log.requestMethod}</TableCell>
163+
<TableCell>{log.endpoint}</TableCell>
164+
<TableCell>{log.handler}</TableCell>
165+
<TableCell>{log.timeConsumed}</TableCell>
166+
<TableCell>{log.userId || '-'}</TableCell>
167+
<TableCell>
168+
<Tooltip title="View Details">
169+
<IconButton
170+
size="small"
171+
onClick={() => setSelectedLogId(log.id)}
172+
>
173+
<VisibilityIcon />
174+
</IconButton>
175+
</Tooltip>
176+
</TableCell>
177+
</TableRow>
178+
))
179+
)}
180+
</TableBody>
181+
</Table>
182+
</TableContainer>
183+
184+
<TablePagination
185+
component="div"
186+
count={totalCount}
187+
page={page}
188+
onPageChange={handlePageChange}
189+
rowsPerPage={rowsPerPage}
190+
rowsPerPageOptions={[10]}
191+
/>
192+
</Card>
193+
194+
<TelemetryLogDetails
195+
open={!!selectedLogId}
196+
onClose={handleCloseDetails}
197+
logId={selectedLogId}
198+
/>
199+
</>
200+
);
201+
};
202+
203+
export default TelemetryLogs;

0 commit comments

Comments
 (0)