mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-01-25 02:08:19 +00:00
support showing call cloudwatch logs (#490)
* support showing call cloudwatch logs * wip * fix review comments * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip
This commit is contained in:
6
.env
6
.env
@@ -1,4 +1,4 @@
|
||||
VITE_API_BASE_URL=http://127.0.0.1:3000/v1
|
||||
#VITE_API_BASE_URL=http://127.0.0.1:3000/v1
|
||||
#VITE_DEV_BASE_URL=http://127.0.0.1:3000/v1
|
||||
|
||||
## enables choosing units and lisenced account call limits
|
||||
@@ -27,4 +27,6 @@ VITE_API_BASE_URL=http://127.0.0.1:3000/v1
|
||||
## Strip publishable key
|
||||
#VITE_APP_STRIPE_PUBLISHABLE_KEY="pk_test_EChRaX9Tjk8csZZVSeoGqNvu00lsJzjaU1"
|
||||
## ignore some specific speech vendors, defined by ADDITIONAL_SPEECH_VENDORS constant
|
||||
# VITE_APP_DISABLE_ADDITIONAL_SPEECH_VENDORS=true
|
||||
# VITE_APP_DISABLE_ADDITIONAL_SPEECH_VENDORS=true
|
||||
## AWS region for enabling Recent Call Feature server logs
|
||||
#VITE_APP_AWS_REGION=us-west-2
|
||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -2561,30 +2561,6 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
@@ -3238,6 +3214,31 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cypress/node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/cypress/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -7205,6 +7206,7 @@
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { hasValue } from "src/utils";
|
||||
import type {
|
||||
CartesiaOptions,
|
||||
Currency,
|
||||
@@ -33,6 +32,7 @@ interface JambonzWindowObject {
|
||||
DEFAULT_SERVICE_PROVIDER_SID: string;
|
||||
STRIPE_PUBLISHABLE_KEY: string;
|
||||
DISABLE_ADDITIONAL_SPEECH_VENDORS: string;
|
||||
AWS_REGION: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -44,9 +44,10 @@ declare global {
|
||||
/** https://vitejs.dev/guide/env-and-mode.html#env-files */
|
||||
const CONFIGURED_API_BASE_URL =
|
||||
window.JAMBONZ?.API_BASE_URL || import.meta.env.VITE_API_BASE_URL;
|
||||
export const API_BASE_URL = hasValue(CONFIGURED_API_BASE_URL)
|
||||
? CONFIGURED_API_BASE_URL
|
||||
: `${window.location.protocol}//${window.location.hostname}/api/v1`;
|
||||
export const API_BASE_URL =
|
||||
CONFIGURED_API_BASE_URL && CONFIGURED_API_BASE_URL.length !== 0
|
||||
? CONFIGURED_API_BASE_URL
|
||||
: `${window.location.protocol}//${window.location.hostname}/api/v1`;
|
||||
|
||||
/** Serves mock API responses from a local dev API server */
|
||||
export const DEV_BASE_URL = import.meta.env.VITE_DEV_BASE_URL;
|
||||
@@ -87,6 +88,9 @@ export const DISABLE_ADDITIONAL_SPEECH_VENDORS: boolean =
|
||||
import.meta.env.VITE_APP_DISABLE_ADDITIONAL_SPEECH_VENDORS || "false",
|
||||
);
|
||||
|
||||
export const AWS_REGION: string =
|
||||
window.JAMBONZ?.AWS_REGION || import.meta.env.VITE_APP_AWS_REGION;
|
||||
|
||||
export const DEFAULT_SERVICE_PROVIDER_SID: string =
|
||||
window.JAMBONZ?.DEFAULT_SERVICE_PROVIDER_SID ||
|
||||
import.meta.env.VITE_APP_DEFAULT_SERVICE_PROVIDER_SID;
|
||||
|
||||
@@ -823,17 +823,19 @@ export const getRecentCalls = (sid: string, query: Partial<CallQuery>) => {
|
||||
const qryStr = getQuery<Partial<CallQuery>>(query);
|
||||
|
||||
return getFetch<PagedResponse<RecentCall>>(
|
||||
import.meta.env.DEV
|
||||
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls?${qryStr}`
|
||||
: `${API_ACCOUNTS}/${sid}/RecentCalls?${qryStr}`,
|
||||
`${API_ACCOUNTS}/${sid}/RecentCalls?${qryStr}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const getRecentCall = (sid: string, sipCallId: string) => {
|
||||
return getFetch<TotalResponse>(
|
||||
import.meta.env.DEV
|
||||
? `${DEV_BASE_URL}/Accounts/${sid}/RecentCalls/${sipCallId}`
|
||||
: `${API_ACCOUNTS}/${sid}/RecentCalls/${sipCallId}`,
|
||||
`${API_ACCOUNTS}/${sid}/RecentCalls/${sipCallId}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const getRecentCallLog = (sid: string, callSid: string) => {
|
||||
return getFetch<string[]>(
|
||||
`${API_ACCOUNTS}/${sid}/RecentCalls/${callSid}/logs`,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
131
src/containers/internal/views/recent-calls/call-system-logs.tsx
Normal file
131
src/containers/internal/views/recent-calls/call-system-logs.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import dayjs from "dayjs";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getRecentCallLog } from "src/api";
|
||||
import { RecentCall } from "src/api/types";
|
||||
import { Icons, Spinner } from "src/components";
|
||||
import { toastError, toastSuccess } from "src/store";
|
||||
import { hasValue } from "src/utils";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
dayjs.extend(utc);
|
||||
|
||||
type CallSystemLogsProps = {
|
||||
call: RecentCall;
|
||||
};
|
||||
|
||||
// Helper function to format logs
|
||||
const formatLog = (log: string): string => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const parsedLog = JSON.parse(log) as any;
|
||||
|
||||
const l = {
|
||||
...parsedLog,
|
||||
time: dayjs(parsedLog.time).utc().format("YYYY-MM-DD HH:mm:ssZ"),
|
||||
};
|
||||
return JSON.stringify(l, null, 2);
|
||||
} catch {
|
||||
return log;
|
||||
}
|
||||
};
|
||||
|
||||
export default function CallSystemLogs({ call }: CallSystemLogsProps) {
|
||||
const [logs, setLogs] = useState<string[] | null>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {}, [call]);
|
||||
const getLogs = () => {
|
||||
setLoading(true);
|
||||
setCount((prev) => prev + 1);
|
||||
if (call && call.account_sid && call.call_sid) {
|
||||
getRecentCallLog(call.account_sid, call.call_sid)
|
||||
.then(({ json }) => {
|
||||
setLogs(json);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.status === 404) {
|
||||
toastError("There is no log for this call");
|
||||
} else {
|
||||
toastError(err.msg);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
if (!logs) {
|
||||
return;
|
||||
}
|
||||
const textToCopy = logs.map(formatLog).join("\n\n");
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(textToCopy)
|
||||
.then(() => toastSuccess("Logs copied to clipboard"))
|
||||
.catch(() => toastError("Failed to copy logs"));
|
||||
};
|
||||
|
||||
const downloadLogs = () => {
|
||||
if (!logs) {
|
||||
return;
|
||||
}
|
||||
const textToDownload = logs.map(formatLog).join("\n\n");
|
||||
|
||||
const blob = new Blob([textToDownload], { type: "text/plain" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `${call.call_sid}.log`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<div className="log-container">
|
||||
<div className="log-buttons">
|
||||
<button
|
||||
onClick={getLogs}
|
||||
className="log-retrieve-button"
|
||||
title="Retrieve Logs"
|
||||
disabled={loading}
|
||||
>
|
||||
<div style={{ display: "flex", gap: "5px" }}>
|
||||
Retrieve Logs
|
||||
{loading && <Spinner small />}
|
||||
</div>
|
||||
</button>
|
||||
{hasValue(logs) && logs.length !== 0 && (
|
||||
<>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="log-button"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<Icons.Clipboard />
|
||||
</button>
|
||||
<button
|
||||
onClick={downloadLogs}
|
||||
className="log-button"
|
||||
title="Download logs"
|
||||
>
|
||||
<Icons.Download />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<pre className="log-content">
|
||||
{hasValue(logs) && logs.length !== 0
|
||||
? logs?.map((log, index) => (
|
||||
<div key={index}>{formatLog(log)}</div>
|
||||
))
|
||||
: count !== 0 && logs === null
|
||||
? "No logs found"
|
||||
: ""}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,9 +8,10 @@ import type { RecentCall } from "src/api/types";
|
||||
import { Tabs, Tab } from "@jambonz/ui-kit";
|
||||
import CallDetail from "./call-detail";
|
||||
import CallTracing from "./call-tracing";
|
||||
import { DISABLE_JAEGER_TRACING } from "src/api/constants";
|
||||
import { AWS_REGION, DISABLE_JAEGER_TRACING } from "src/api/constants";
|
||||
import { Player } from "./player";
|
||||
import "./styles.scss";
|
||||
import CallSystemLogs from "./call-system-logs";
|
||||
|
||||
type DetailsItemProps = {
|
||||
call: RecentCall;
|
||||
@@ -78,6 +79,13 @@ export const DetailsItem = ({ call }: DetailsItemProps) => {
|
||||
<Tab id="tracing" label="Tracing">
|
||||
{open && <CallTracing call={call} />}
|
||||
</Tab>
|
||||
{hasValue(AWS_REGION) ? (
|
||||
<Tab id="logs" label="Logs">
|
||||
{open && <CallSystemLogs call={call} />}
|
||||
</Tab>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Tabs>
|
||||
)}
|
||||
{open && (
|
||||
|
||||
@@ -85,3 +85,82 @@
|
||||
margin-top: ui-vars.$px01;
|
||||
}
|
||||
}
|
||||
|
||||
/* CallSystemLogs.css */
|
||||
|
||||
/* Styles for the log container */
|
||||
.log-container {
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
background: #1a1a1a; /* Dark background for the container (optional, if you want the entire container dark) */
|
||||
color: #ffffff; /* Ensure text is visible on dark background */
|
||||
}
|
||||
|
||||
/* Styles for the log buttons container (optional, if you want to style it separately) */
|
||||
.log-buttons {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 25px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Styles for the log content (pre element) */
|
||||
.log-content {
|
||||
margin-top: 16px;
|
||||
background: #1a1a1a; /* Darker background for the log content */
|
||||
|
||||
overflow: auto;
|
||||
min-height: 250px;
|
||||
max-height: 800px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); /* Slightly darker shadow for contrast */
|
||||
color: #e0e0e0; /* Light gray text for visibility on dark background */
|
||||
}
|
||||
|
||||
/* Optional: Style for individual log entries (divs within pre) */
|
||||
.log-content div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Styles for log buttons */
|
||||
.log-button {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #fff3f6; /* Light gray background for buttons, unchanged */
|
||||
color: #da1c5c;
|
||||
transition: transform 0.1s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.log-retrieve-button {
|
||||
@extend .log-button;
|
||||
border-radius: 8px;
|
||||
width: auto;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Hover state for buttons */
|
||||
.log-button:hover {
|
||||
background: #d5d5d5;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.log-fetch-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%; /* Adjust based on your layout */
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user