/jest.setup.js'],
14 |
15 | testEnvironment: 'jest-environment-jsdom',
16 | };
17 |
18 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
19 | export default createJestConfig(config);
20 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 |
3 | const nextConfig = {
4 | async headers() {
5 | return [
6 | {
7 | // matching all API routes
8 | source: '/api/:path*',
9 | headers: [
10 | { key: 'Access-Control-Allow-Credentials', value: 'true' },
11 | { key: 'Access-Control-Allow-Origin', value: '*' },
12 | { key: 'Access-Control-Allow-Methods', value: 'GET,OPTIONS,PATCH,DELETE,POST,PUT' },
13 | {
14 | key: 'Access-Control-Allow-Headers',
15 | value:
16 | 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version',
17 | },
18 | ],
19 | },
20 | ];
21 | },
22 | };
23 |
24 | module.exports = nextConfig;
25 |
--------------------------------------------------------------------------------
/otelController.js:
--------------------------------------------------------------------------------
1 | const otelController = {};
2 |
3 | //helper function - identifies strings with substrings that match array elements
4 | const includesAny = (array, string) => {
5 | for (let i = 0; i < array.length; i++) {
6 | if (string.includes(array[i])) return true;
7 | }
8 | return false;
9 | };
10 |
11 | //middleware to handle parsing HTTP requests
12 | const parseHTTP = (clientData, spans) => {
13 | const ignoreEndpoints = ['localhost', 'socket', 'nextjs']; //endpoints to ignore
14 |
15 | //add specific span data to clientData array through deconstruction of span elements
16 | //spans is an array of span objects
17 | //attributes is an array of nested object with one key-value pair per array element
18 | //ex: attributes = [{key: 'http.url', value: {stringValue: 'wwww.api.com/'}}...]
19 | //el.attributes.find finds the array element with a matching key desired and returns the unnested value if
20 | //exists or null if doesn't exist
21 | spans.forEach((el) => {
22 | const clientObj = {
23 | spanId: el.spanId,
24 | traceId: el.traceId,
25 | startTime: Math.floor(el.startTimeUnixNano / Math.pow(10, 6)), //[ms]
26 | duration: Math.floor((el.endTimeUnixNano - el.startTimeUnixNano) / Math.pow(10, 6)), //[ms]
27 | contentLength: (() => {
28 | const packageObj = el.attributes.find((attr) => attr.key === 'contentLength');
29 | const size = packageObj ? packageObj.value.intValue : 0;
30 | return size;
31 | })(),
32 | statusCode: el.attributes.find((attr) => attr.key === 'http.status_code')?.value?.intValue,
33 | endPoint: el.attributes.find((attr) => attr.key === 'http.url')?.value?.stringValue,
34 | requestMethod: el.name,
35 | requestType: 'HTTPS',
36 | };
37 |
38 | //if the endpoint is an external api add it to client data array
39 | if (clientObj.endPoint) {
40 | if (!includesAny(ignoreEndpoints, clientObj.endPoint)) {
41 | clientData.push(clientObj);
42 | }
43 | }
44 | });
45 | return clientData;
46 | };
47 |
48 | //middleware to handle parsing mongoose requests
49 | const parseMongoose = (clientData, spans) => {
50 | //iterate through array of OTLP objects pulling desired attri
51 | spans.forEach((el) => {
52 | //find package size of individual request
53 | let tempPackageSize;
54 |
55 | const clientObj = {
56 | spanId: el.spanId,
57 | traceId: el.traceId,
58 | startTime: Math.floor(el.startTimeUnixNano / Math.pow(10, 6)), //[ms]
59 | duration: Math.floor((el.endTimeUnixNano - el.startTimeUnixNano) / Math.pow(10, 6)), //[ms]
60 | contentLength: (() => {
61 | const packageObj = el.attributes.find((attr) => attr.key === 'contentLength');
62 | const size = packageObj ? packageObj.value.intValue : 0;
63 | tempPackageSize = size;
64 | return size;
65 | })(),
66 | statusCode: tempPackageSize ? 200 : 404,
67 | endPoint: el.attributes.find((attr) => attr.key === 'db.mongodb.collection')?.value
68 | ?.stringValue,
69 | requestMethod: el.attributes.find((attr) => attr.key === 'db.operation')?.value?.stringValue,
70 | requestType: 'Mongoose',
71 | };
72 | clientData.push(clientObj);
73 | });
74 | return clientData;
75 | };
76 |
77 | //middleware to handle parsing pg requests
78 | const parsePg = (clientData, spans) => {
79 | //iterate through array of OTLP objects pulling desired attri
80 | spans.forEach((el) => {
81 | //find package size of individual request
82 | let tempPackageSize;
83 |
84 | const clientObj = {
85 | spanId: el.spanId,
86 | traceId: el.traceId,
87 | startTime: Math.floor(el.startTimeUnixNano / Math.pow(10, 6)), //[ms]
88 | duration: Math.floor((el.endTimeUnixNano - el.startTimeUnixNano) / Math.pow(10, 6)), //[ms]
89 | contentLength: (() => {
90 | const packageObj = el.attributes.find((attr) => attr.key === 'contentLength');
91 | const size = packageObj ? packageObj.value.intValue : 0;
92 | tempPackageSize = size;
93 | return size;
94 | })(),
95 | statusCode: tempPackageSize ? 200 : 404,
96 | endPoint: el.attributes.find((attr) => attr.key === 'db.name')?.value?.stringValue,
97 | requestMethod: el.attributes
98 | .find((attr) => attr.key === 'db.statement')
99 | ?.value?.stringValue.split(' ')[0],
100 | requestType: 'PostgreSQL',
101 | };
102 | clientData.push(clientObj);
103 | });
104 | return clientData;
105 | };
106 |
107 | otelController.parseTrace = (req, res, next) => {
108 | let clientData = [];
109 | const spans = req.body.resourceSpans[0].scopeSpans[0].spans;
110 |
111 | const instrumentationLibrary = spans[0].attributes.find(
112 | (attr) => attr.key === 'instrumentationLibrary'
113 | )?.value?.stringValue;
114 |
115 | //invoke different middleware function based on instrument used to collect incoming trace
116 | //middleware functions will deconstruct request body and built out clientData array
117 | switch (instrumentationLibrary) {
118 | case '@opentelemetry/instrumentation-mongoose':
119 | clientData = parseMongoose(clientData, spans);
120 | break;
121 | case '@opentelemetry/instrumentation-http':
122 | clientData = parseHTTP(clientData, spans);
123 | break;
124 | case '@opentelemetry/instrumentation-pg':
125 | clientData = parsePg(clientData, spans);
126 | break;
127 | default:
128 | break;
129 | }
130 |
131 | res.locals.clientData = clientData;
132 | return next();
133 | };
134 |
135 | module.exports = otelController;
136 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "datatrace",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "nodemon --require ./tracing.js ./node_modules/.bin/next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "test": "jest --runInBand",
11 | "print": "echo 'hello all'",
12 | "prepare": "husky install",
13 | "fix:lint": "next lint -- --fix",
14 | "fix:format": "prettier --write .",
15 | "fix:tsc": "tsc --noEmit"
16 | },
17 | "dependencies": {
18 | "@emotion/react": "^11.10.6",
19 | "@emotion/styled": "^11.10.6",
20 | "@mui/icons-material": "^5.11.11",
21 | "@mui/material": "^5.11.15",
22 | "@observablehq/plot": "^0.6.5",
23 | "@opentelemetry/exporter-trace-otlp-http": "^0.37.0",
24 | "@opentelemetry/instrumentation": "^0.37.0",
25 | "@opentelemetry/instrumentation-http": "^0.36.1",
26 | "@opentelemetry/instrumentation-mongoose": "^0.32.1",
27 | "@opentelemetry/instrumentation-pg": "^0.35.0",
28 | "@opentelemetry/sdk-trace-node": "^1.11.0",
29 | "@types/node": "^18.15.11",
30 | "@types/react": "^18.0.31",
31 | "@types/react-dom": "^18.0.11",
32 | "chart.js": "^4.2.1",
33 | "chartjs-adapter-date-fns": "^3.0.0",
34 | "cors": "^2.8.5",
35 | "d3": "^7.8.4",
36 | "date-fns": "^2.29.3",
37 | "dotenv": "^16.0.3",
38 | "express": "^4.18.2",
39 | "material-react-table": "^1.9.2",
40 | "mongoose": "^6.10.5",
41 | "next": "13.2.4",
42 | "node-fetch": "^3.3.1",
43 | "pg": "^8.10.0",
44 | "react": "18.2.0",
45 | "react-chartjs-2": "^5.2.0",
46 | "react-dom": "18.2.0",
47 | "socket.io": "^4.6.1",
48 | "socket.io-client": "^4.6.1"
49 | },
50 | "devDependencies": {
51 | "@testing-library/jest-dom": "^5.16.5",
52 | "@testing-library/react": "^14.0.0",
53 | "@types/d3": "^7.4.0",
54 | "@types/node": "^18.15.11",
55 | "@types/react": "^18.0.31",
56 | "@types/react-dom": "^18.0.11",
57 | "@typescript-eslint/eslint-plugin": "^5.57.1",
58 | "@typescript-eslint/parser": "^5.57.1",
59 | "eslint": "^8.37.0",
60 | "eslint-config-airbnb": "^19.0.4",
61 | "eslint-config-airbnb-typescript": "^17.0.0",
62 | "eslint-config-next": "^13.2.4",
63 | "eslint-config-prettier": "^8.8.0",
64 | "eslint-plugin-import": "^2.27.5",
65 | "eslint-plugin-jsx-a11y": "^6.7.1",
66 | "eslint-plugin-react": "^7.32.2",
67 | "eslint-plugin-react-hooks": "^4.6.0",
68 | "husky": "^8.0.0",
69 | "jest": "^27.0.0",
70 | "jest-environment-jsdom": "^27.0.0",
71 | "prettier": "2.8.7",
72 | "typescript": "^5.0.3"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/pages/_DetailList.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import MaterialReactTable from 'material-react-table'; // For resizing & auto sorting columns - Move to detail
4 | import styles from '@/styles/DetailList.module.css';
5 |
6 | export default function DetailList({ columns, data }: any) {
7 | return (
8 |
9 | {/* Data is passed via data, column info passed via columns */}
10 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/pages/_MainWaterfall.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import styles from '@/styles/MainWaterfall.module.css';
4 | import * as d3 from 'd3';
5 | import * as Plot from '@observablehq/plot';
6 | import { useEffect, useRef } from 'react';
7 | import { DataType } from '../types';
8 | import errColor from './functions/errColor';
9 | import tooltips from './functions/tooltip';
10 |
11 | // Component renders the main timeline chart
12 | export default function MainWaterfall(props: any) {
13 | const svgRef: any = useRef(null);
14 |
15 | let svgWidth: any;
16 | let svgHeight: any;
17 |
18 | // Function to create gantt chart - takes in the data passed down from state
19 | function makeGanttChart(data: DataType[]) {
20 | if (data.length === 0) {
21 | return;
22 | }
23 |
24 | // p Creates the plot object - is wrapped in another function that creates tooltip behavior based on info in 'title'
25 | const p: any = tooltips(
26 | Plot.plot({
27 | width: svgWidth,
28 | height: svgHeight,
29 | marks: [
30 | Plot.axisX({ color: '#ced4da', anchor: 'top' }),
31 | Plot.barX(data, {
32 | x1: (d) => d.startTime,
33 | x2: (d) => d.startTime + d.duration,
34 | y: (d) => d.spanId,
35 | rx: 1,
36 | fill: (d) => errColor(d.contentLength, d.statusCode),
37 | title: (d) => `${d.endPoint} | ${d.duration}ms`,
38 | stroke: '#212529',
39 | strokeWidth: 1,
40 | }),
41 | Plot.gridX({ stroke: '#ced4da', strokeOpacity: 0.2 }),
42 | ],
43 | x: { label: 'ms', tickFormat: (e) => `${e} ms` },
44 | y: { axis: null, paddingOuter: 5 }, // 10 is as large as you should go for the padding base - if there are large numbers of the bars gets too small
45 | })
46 | );
47 |
48 | // Selects current timeline, removes it, then adds the newly created one on state updates
49 | d3.select(svgRef.current).selectAll('*').remove();
50 | if (p) {
51 | d3.select(svgRef.current).append(() => p);
52 | }
53 | }
54 | // Bases size of svg from observable on the current div size
55 | useEffect(() => {
56 | const { data } = props;
57 | if (svgRef.current) {
58 | const dimensions = svgRef.current.getBoundingClientRect();
59 | svgWidth = dimensions.width;
60 | svgHeight = dimensions.height;
61 | }
62 | makeGanttChart(data);
63 | }, [props?.data]);
64 |
65 | return ;
66 | }
67 |
--------------------------------------------------------------------------------
/pages/_Sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import styles from '@/styles/Sidebar.module.css';
4 |
5 | export default function Sidebar() {
6 | return (
7 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css';
2 | import type { AppProps } from 'next/app';
3 | import { createTheme, ThemeProvider } from '@mui/material';
4 |
5 | export default function App({ Component, pageProps }: AppProps) {
6 | const theme = createTheme({
7 | palette: {
8 | mode: 'dark',
9 | primary: {
10 | main: '#212529',
11 | },
12 | secondary: {
13 | main: '#212529',
14 | },
15 | background: {
16 | default: '#212529',
17 | paper: '#212529',
18 | },
19 | success: {
20 | main: '#212529',
21 | },
22 | },
23 | });
24 |
25 | return (
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/pages/api/dummyDemo.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next';
3 |
4 | import fetch from 'node-fetch';
5 | // coerce next.js 'fetch' to node-fetch so it is picked up by opentelemetry
6 | const { Movie } = require('../../tracing');
7 | const { pool } = require('../../tracing');
8 |
9 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
10 | // make many fetches at once
11 | const response = await fetch(`https://swapi.dev/api/people/4`);
12 | const response2 = await fetch(`https://zenquotes.io/api/today`);
13 | const response3 = await fetch(`https://api.adviceslip.com/advice`);
14 | const response16 = await fetch(`https://swapi.dev/api/blanker`);
15 | const response4 = await fetch(`https://api.adviceslip.com/advice`);
16 | const users1 = await pool.query('SELECT * FROM users');
17 | const response5 = await fetch(`http://swapi.co/api/films`);
18 | const response6 = await fetch(`https://dog.ceo/api/breeds/image/random`);
19 | await Movie.find({});
20 | const response7 = await fetch(`https://randombig.cat/roar.json`);
21 | const response8 = await fetch(`https://api.apis.guru/v2/list.json`);
22 | const users2 = await pool.query('SELECT * FROM users');
23 | const response9 = await fetch(`https://www.gov.uk/bank-holidays.json`);
24 | const response10 = await fetch(`https://api.coinbase.com/v2/currencies`);
25 | const response11 = await fetch(`https://api.coinlore.net/api/tickers/`);
26 | const response12 = await fetch(`https://www.cryptingup.com/api/markets`);
27 | const response13 = await fetch(`https://api.exchangerate.host/latest`);
28 | await Movie.find({});
29 | const response14 = await fetch(`https://api.kraken.com/0/public/Trades?pair=ltcusd`);
30 | const response15 = await fetch(`https://favicongrabber.com/api/grab/github.com`);
31 |
32 | const data1 = await response.json();
33 | const data2 = await response2.json();
34 | const data3 = await response3.json();
35 | const data4 = await response4.json();
36 | const data5 = await response5.json();
37 | const data6 = await response6.json();
38 | const data7 = await response7.json();
39 | const data8 = await response8.json();
40 | const data9 = await response9.json();
41 | const data10 = await response10.json();
42 | const data11 = await response11.json();
43 | const data12 = await response12.json();
44 | const data13 = await response13.json();
45 | const data14 = await response14.json();
46 | const data15 = await response15.json();
47 | const data16 = await response16.json();
48 |
49 | const data: any[] = [
50 | data1,
51 | data2,
52 | data3,
53 | data4,
54 | data5,
55 | data6,
56 | data7,
57 | data8,
58 | data9,
59 | data10,
60 | data11,
61 | data12,
62 | data13,
63 | data14,
64 | data15,
65 | data16,
66 | users1,
67 | users2,
68 | ];
69 |
70 | res.status(200).json(data);
71 | }
72 |
--------------------------------------------------------------------------------
/pages/api/dummyFetch.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next';
3 | import fetch from 'node-fetch'; // coerce next.js 'fetch' to node-fetch so it is picked up by opentelemetry
4 |
5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
6 | // make many fetches at once
7 | const response = await fetch(`https://swapi.dev/api/people/4`);
8 | const response2 = await fetch(`https://zenquotes.io/api/today`);
9 | const response3 = await fetch(`https://api.adviceslip.com/advice`);
10 | const response4 = await fetch(`https://api.adviceslip.com/advice`);
11 | const response5 = await fetch(`https://swapi.co/api/films`);
12 | const response6 = await fetch(`https://dog.ceo/api/breeds/image/random`);
13 | const response7 = await fetch(`https://randombig.cat/roar.json`);
14 | const response8 = await fetch(`https://api.apis.guru/v2/list.json`);
15 | const response9 = await fetch(`https://www.gov.uk/bank-holidays.json`);
16 | const response10 = await fetch(`https://api.coinbase.com/v2/currencies`);
17 | const response11 = await fetch(`https://api.coinlore.net/api/tickers/`);
18 | const response12 = await fetch(`https://www.cryptingup.com/api/markets`);
19 | const response13 = await fetch(`https://api.exchangerate.host/latest`);
20 | const response14 = await fetch(`https://api.kraken.com/0/public/Trades?pair=ltcusd`);
21 | const response15 = await fetch(`https://favicongrabber.com/api/grab/github.com`);
22 |
23 | const data1 = await response.json();
24 | const data2 = await response2.json();
25 | const data3 = await response3.json();
26 | const data4 = await response4.json();
27 | const data5 = await response5.json();
28 | const data6 = await response6.json();
29 | const data7 = await response7.json();
30 | const data8 = await response8.json();
31 | const data9 = await response9.json();
32 | const data10 = await response10.json();
33 | const data11 = await response11.json();
34 | const data12 = await response12.json();
35 | const data13 = await response13.json();
36 | const data14 = await response14.json();
37 | const data15 = await response15.json();
38 |
39 | const data: any[] = [
40 | data1,
41 | data2,
42 | data3,
43 | data4,
44 | data5,
45 | data6,
46 | data7,
47 | data8,
48 | data9,
49 | data10,
50 | data11,
51 | data12,
52 | data13,
53 | data14,
54 | data15,
55 | ];
56 |
57 | res.status(200).json(data);
58 | }
59 |
--------------------------------------------------------------------------------
/pages/api/dummyMongoose.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next';
3 |
4 | const { Movie } = require('../../tracing');
5 |
6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
7 | await Movie.find({})
8 | .then((data: any) => res.status(200).json(data[0]))
9 | .catch((err: any) => {
10 | console.log('error:', err);
11 | return res.status(404).json('Mongoose Error');
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/pages/api/dummyPg.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next';
3 |
4 | const { pool } = require('../../tracing');
5 |
6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
7 | const users = await pool.query('SELECT * FROM users');
8 | res.status(200).json(users.rows);
9 | }
10 |
--------------------------------------------------------------------------------
/pages/functions/errColor.ts:
--------------------------------------------------------------------------------
1 | export default function errColor(contentLength: number, statusCode: number): string {
2 | if (!statusCode) return 'red';
3 | const strStatus: string = statusCode.toString();
4 | if (contentLength > 1 && strStatus[0] === '2') return 'green';
5 | if (strStatus[0] === '3') return '#34dbeb';
6 | if (strStatus[0] === '4') return 'red';
7 | if (strStatus[0] === '5') return 'red';
8 | return 'red';
9 | }
10 |
--------------------------------------------------------------------------------
/pages/functions/tooltip.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 | // Exported as function -> if waterfall chart is refactored into observable, can easily wrap Plot.plot() with this function to activate tooltips
3 | // NOTE - When observable native tooltip functionality is added, remove this and use the native functionality instead.
4 |
5 | export default function tooltips(chart) {
6 | // selects the whole chart
7 | const wrap = d3.select(chart);
8 | // creates a tooltip element, assigns it a hover class, joins it to a group of hover
9 | const tooltip = wrap.selectAll('.hover').data([1]).join('g').attr('class', 'hover');
10 |
11 | wrap.selectAll('title').each(function () {
12 | // Takes text from title attr, sets it as an attr on parent node if it exists, then cleans up by removing it.
13 | const title = d3.select(this);
14 | const parentNode = d3.select(this.parentNode);
15 |
16 | if (title.text()) {
17 | parentNode.attr('titletext', title.text()).classed('has-title', true);
18 | title.remove();
19 | }
20 | // Governs pointer movement behavior interaction w parent node of title
21 | parentNode
22 | .on('pointerenter pointermove', function (event) {
23 | const text = d3.select(this).attr('titletext');
24 | const pointer = d3.pointer(event, wrap.node());
25 | if (text) tooltip.call(hover, pointer, text.split('\n'));
26 | else tooltip.selectAll('*').remove();
27 |
28 | // Raise it
29 | d3.select(this).raise();
30 | // Disappears under toolbar without this
31 |
32 | // Checks position of the tip's x & y, translates box position if it would be outside the div
33 | const tipSize = tooltip.node().getBBox();
34 | // Left side logic
35 | if (pointer[0] + tipSize.x < 0)
36 | tooltip.attr('transform', `translate(${tipSize.width / 2}, ${pointer[1] + 7})`);
37 | // Right side logic
38 | else if (pointer[0] + tipSize.width / 2 > wrap.attr('width'))
39 | tooltip.attr(
40 | 'transform',
41 | `translate(${wrap.attr('width') - tipSize.width / 2}, ${pointer[1] + 7})`
42 | );
43 | })
44 | .on('pointerout', function (event) {
45 | tooltip.selectAll('*').remove();
46 | });
47 | });
48 |
49 | // Hover function
50 | const hover = (tooltip, pos, text) => {
51 | const side_padding = 5;
52 | const vertical_padding = 5;
53 | const vertical_offset = 17;
54 |
55 | // Removes hover element as it moves - endleslly expands without this
56 | tooltip.selectAll('*').remove();
57 |
58 | // Creates text based on position
59 | tooltip
60 | .style('text-anchor', 'middle')
61 | .attr('transform', `translate(${pos[0]}, ${pos[1] + 7})`)
62 | .selectAll('text')
63 | .data(text)
64 | .join('text')
65 | .style('dominant-baseline', 'ideographic')
66 | .text((d) => d)
67 | .attr('y', (d, i) => (i - (text.length - 1)) * 15 - vertical_offset)
68 | .style('font-weight', (d, i) => (i === 0 ? 'bold' : 'normal'));
69 |
70 | // Grabs the size of the box created by the text
71 |
72 | const bbox = tooltip.node().getBBox();
73 |
74 | // Governs tooltip rear -> currently creates a rectangle based on side & vertical positions
75 | // .style determines background, stroke determines text color
76 | // FUTURE ADDITION: option to change styles to preferences?
77 | tooltip
78 | .append('rect')
79 | .attr('x', bbox.x - side_padding)
80 | .attr('y', bbox.y - vertical_padding)
81 | .attr('width', bbox.width + side_padding * 2)
82 | .attr('height', bbox.height + vertical_padding * 2)
83 | .style('fill', 'white')
84 | .lower(); // Moves the rectangle below the text - DO NOT REMOVE
85 | };
86 |
87 | return chart;
88 | }
89 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | // import react and nextjs packages
4 | import Head from 'next/head';
5 | import React, { useCallback, useMemo, useState, useEffect } from 'react';
6 |
7 | // import types
8 | import type { MRT_ColumnDef } from 'material-react-table';
9 |
10 | // Material-UI Imports
11 | import { Box } from '@mui/material';
12 |
13 | // import socket client
14 | import { io } from 'socket.io-client';
15 | import styles from '@/styles/Home.module.css';
16 |
17 | // import child components
18 | // import { Inter } from 'next/font/google';
19 | import Sidebar from './_Sidebar';
20 | import MainWaterfall from './_MainWaterfall';
21 | import DetailList from './_DetailList';
22 |
23 | // import type
24 | import { DataType } from '../types';
25 |
26 | // import functions
27 | import errColor from './functions/errColor';
28 |
29 | // const inter = Inter({ subsets: ['latin'] });
30 |
31 | // Main Component - Home
32 | export default function Home() {
33 | // Hook for updating overall time and tying it to state
34 | // Time is determined by the difference between the final index's start+duration minus the initial index's start
35 | let initialStartTime: number;
36 | const [data, setData] = useState([]);
37 |
38 | // intialize socket connection
39 | // when data recieved update state here
40 | const socketInitializer = useCallback(async () => {
41 | const socket = await io('http://localhost:4000/');
42 | socket.on('connect', () => {
43 | console.log('socket connected.');
44 | });
45 | socket.on('message', (msg) => {
46 | // when data recieved concat messages state with inbound traces
47 | const serverTraces: DataType[] = JSON.parse(msg);
48 | serverTraces.forEach((el: DataType) => {
49 | const newEl = { ...el };
50 | // TODO: change the below to check for equal to 0 when we get rid of starter data
51 | if (initialStartTime === undefined) {
52 | initialStartTime = el.startTime;
53 | }
54 | if (el.contentLength === null) newEl.contentLength = 1;
55 | newEl.startTime -= initialStartTime;
56 | setData((prev: DataType[]) => [...prev, newEl]);
57 | });
58 | });
59 | }, [setData]);
60 |
61 | // when home component mounts initialize socket connection
62 | useEffect(() => {
63 | socketInitializer();
64 | }, []);
65 |
66 | // an empty array that collects barData objects in the below for loop
67 | const barDataSet = [];
68 |
69 | // generates barData object for each sample data from data array with label being the endpoint
70 | // data takes in the exact start-time and total time
71 | for (let i = 0; i < data.length; i++) {
72 | barDataSet.push({
73 | label: [data[i].endPoint],
74 | data: [
75 | {
76 | x: [data[i].startTime, data[i].startTime + data[i].duration],
77 | y: 1,
78 | },
79 | ],
80 | backgroundColor: ['green'],
81 | borderColor: ['limegreen'],
82 | });
83 | }
84 |
85 | // Create columns -> later on, we can dynamically declare this based
86 | // on user options using a config file or object or state and only
87 | // rendering the things that are requested
88 |
89 | // Column declaration requires a flat array of objects with a header
90 | // which is the column's title, and an accessorKey, which is the
91 | // key in the data object.
92 | const columns = useMemo[]>(
93 | () => [
94 | {
95 | header: 'Endpoint',
96 | accessorKey: 'endPoint',
97 | },
98 | {
99 | header: 'Status',
100 | accessorKey: 'statusCode',
101 | },
102 | {
103 | header: 'Type',
104 | accessorKey: 'requestType',
105 | },
106 | {
107 | header: 'Method',
108 | accessorKey: 'requestMethod',
109 | },
110 | {
111 | header: 'Size (B)',
112 | accessorKey: 'contentLength',
113 | },
114 | {
115 | header: 'Start (ms)',
116 | accessorKey: 'startTime',
117 | },
118 | {
119 | header: 'Duration (ms)',
120 | accessorKey: 'duration',
121 | },
122 | {
123 | header: 'TraceID',
124 | accessorKey: 'traceId',
125 | },
126 | {
127 | header: 'Waterfall',
128 | accessorKey: 'spanId',
129 | enablePinning: true,
130 | minSize: 200, // min size enforced during resizing
131 | maxSize: 1000, // max size enforced during resizing
132 | size: 300, // medium column
133 | // custom conditional format and styling
134 | // eslint-disable-next-line
135 | Cell: ({ cell, row }) => (
136 | ({
139 | // eslint-disable-next-line
140 | backgroundColor: errColor(row.original.contentLength!, row.original.statusCode),
141 | borderRadius: '0.1rem',
142 | color: 'transparent',
143 | // We first select the cell, then determine the left and right portions and make it a percentage
144 | //
145 | marginLeft: (() => {
146 | const cellStartTime = row.original.startTime;
147 | const totalTime = data.length
148 | ? data[data.length - 1].startTime + data[data.length - 1].duration
149 | : cellStartTime;
150 | const pCellTotal = (cellStartTime / totalTime) * 100;
151 | return `${pCellTotal}%`;
152 | })(),
153 | width: (() => {
154 | const cellDuration = row.original.duration;
155 | const totalTime = data.length
156 | ? data[data.length - 1].startTime + data[data.length - 1].duration
157 | : cellDuration;
158 | const pCellDuration = (cellDuration / totalTime) * 100;
159 | return `${pCellDuration}%`;
160 | })(),
161 | })}
162 | >
163 | {/* The | mark is required to mount & render the boxes */}|
164 |
165 | ),
166 | },
167 | ],
168 | [data]
169 | );
170 |
171 | return (
172 | <>
173 |
174 | NetPulse Dashboard
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 | >
186 | );
187 | }
188 |
--------------------------------------------------------------------------------
/public/images/netpulseicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/NetPulse/0b2d81f1df05574e6d23c0c5a749a1b0863f7620/public/images/netpulseicon.png
--------------------------------------------------------------------------------
/styles/DetailList.module.css:
--------------------------------------------------------------------------------
1 | .detailList {
2 | background-color: #212529;
3 | height: 85%;
4 | border: solid 1px #212529;
5 | /* border-radius: 1rem; */
6 | /* overflow-y: scroll; */
7 | }
8 |
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | /* flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center; */
6 | /* padding: 1rem; */
7 | min-height: 100vh;
8 | background-color: #212529;
9 | }
10 |
11 | .networkContainer {
12 | width: 100%;
13 | /* padding: 1rem; */
14 | /* border-radius: 1rem; */
15 | }
16 |
17 | @media (min-width: 701px) and (max-width: 1120px) {
18 | .grid {
19 | grid-template-columns: repeat(2, 50%);
20 | }
21 | }
22 |
23 | @keyframes rotate {
24 | from {
25 | transform: rotate(360deg);
26 | }
27 |
28 | to {
29 | transform: rotate(0deg);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/styles/MainWaterfall.module.css:
--------------------------------------------------------------------------------
1 | .chart {
2 | width: 100%;
3 | height: 15%;
4 | }
5 |
6 | .highlight {
7 | stroke: white;
8 | stroke-width: 4;
9 | }
10 |
--------------------------------------------------------------------------------
/styles/Sidebar.module.css:
--------------------------------------------------------------------------------
1 | .sidebar {
2 | font-family: Arial, Helvetica, sans-serif;
3 | display: flex;
4 | flex-direction: column;
5 | align-items: center;
6 | /* justify-content: space-around; */
7 | background-color: #212529;
8 | width: 10%;
9 | min-width: 120px;
10 |
11 | border: solid 1px #495057;
12 | color: #ced4da;
13 | }
14 |
15 | .Data {
16 | color: #266ed0;
17 | }
18 |
19 | .mainLogo {
20 | margin-top: 10%;
21 | display: flex;
22 | flex-direction: column;
23 | align-items: center;
24 | }
25 |
26 | /* .showMenu {
27 | color: #396087;
28 | font-weight: bold;
29 | display: none;
30 | width: 2rem;
31 | height: 2rem;
32 | } */
33 |
34 | .sbLogo {
35 | margin-top: 0.5rem;
36 | width: 90%;
37 | padding: 2%;
38 | height: auto;
39 | /* border-radius: 1rem; */
40 | /* margin-bottom: 10rem; */
41 | }
42 |
43 | .sbContent {
44 | display: flex;
45 | flex-direction: column;
46 | align-items: center;
47 | justify-content: space-between;
48 | height: 100%;
49 | margin: 10%;
50 | }
51 |
52 | .mediumLogo {
53 | display: flex;
54 | width: 3rem;
55 | /* margin-top: 10rem; */
56 | margin-bottom: 1rem;
57 | }
58 |
59 | .npmLogo {
60 | display: flex;
61 | width: 3rem;
62 | margin-top: 0.5rem;
63 | margin-bottom: 1rem;
64 | }
65 |
66 | .githubLogo {
67 | display: flex;
68 | width: 3rem;
69 | margin-top: 0.5rem;
70 | margin-bottom: 1rem;
71 | }
72 |
73 | .hideMenu {
74 | align-self: flex-end;
75 | justify-self: start;
76 | font-weight: bold;
77 | color: #495057;
78 | margin: 0.4rem;
79 | }
80 |
81 | .sidebarButtons {
82 | background-color: rgb(13, 24, 144);
83 | border-radius: 0.3rem;
84 | width: 90%;
85 | padding-bottom: 1rem;
86 | }
87 |
88 | .textLinks {
89 | display: flex;
90 | flex-direction: column;
91 | align-items: center;
92 | margin-top: -250%;
93 | }
94 |
95 | .sbLinks {
96 | margin: 30% 10%;
97 | display: block;
98 | font-size: 1rem;
99 | color: #ced4da;
100 | }
101 |
102 | .logoLinks {
103 | position: static;
104 | display: flex;
105 | flex-direction: column;
106 | justify-self: end;
107 | }
108 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html,
6 | body {
7 | max-width: 100vw;
8 | overflow-x: hidden;
9 | max-height: 100vh;
10 | background-color: black;
11 | margin: 0;
12 | }
13 |
14 | a {
15 | color: inherit;
16 | text-decoration: none;
17 | }
18 |
--------------------------------------------------------------------------------
/styles/theme.ts:
--------------------------------------------------------------------------------
1 | import { ThemeOptions } from '@mui/material/styles';
2 |
3 | export const themeOptions: any = {
4 | palette: {
5 | mode: 'dark',
6 | primary: {
7 | main: '#212529',
8 | },
9 | secondary: {
10 | main: '#212529',
11 | },
12 | background: {
13 | default: '#212529',
14 | paper: '#212529',
15 | },
16 | success: {
17 | main: '#212529',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/tracing.js:
--------------------------------------------------------------------------------
1 | //open telemetry packages
2 | require('dotenv').config();
3 | const { NodeTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-node');
4 | const { registerInstrumentations } = require('@opentelemetry/instrumentation');
5 | const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
6 | const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
7 |
8 | //mongoose instrumentation
9 | const { MongooseInstrumentation } = require('@opentelemetry/instrumentation-mongoose');
10 | const mongoose = require('mongoose');
11 |
12 | //pg instrumentation
13 | const { PgInstrumentation } = require('@opentelemetry/instrumentation-pg');
14 | const { Pool } = require('pg');
15 |
16 | // --- OPEN TELEMETRY SETUP --- //
17 |
18 | const provider = new NodeTracerProvider();
19 |
20 | //register instruments
21 | //inject custom custom attributes for package size and instrumentation library used
22 | //for use in otleController middlware
23 | registerInstrumentations({
24 | instrumentations: [
25 | new HttpInstrumentation({
26 | responseHook: (span, res) => {
27 | span.setAttribute('instrumentationLibrary', span.instrumentationLibrary.name);
28 |
29 | // Get the length of the 8-bit byte array. Size indicated the number of bytes of data
30 | let size = 0;
31 | res.on('data', (chunk) => {
32 | size += chunk.length;
33 | });
34 |
35 | res.on('end', () => {
36 | span.setAttribute('contentLength', size);
37 | });
38 | },
39 | }),
40 | new MongooseInstrumentation({
41 | responseHook: (span, res) => {
42 | span.setAttribute('contentLength', Buffer.byteLength(JSON.stringify(res.response)));
43 | span.setAttribute('instrumentationLibrary', span.instrumentationLibrary.name);
44 | },
45 | }),
46 | new PgInstrumentation({
47 | responseHook: (span, res) => {
48 | span.setAttribute('contentLength', Buffer.byteLength(JSON.stringify(res.data.rows)));
49 | span.setAttribute('instrumentationLibrary', span.instrumentationLibrary.name);
50 | },
51 | }),
52 | ],
53 | });
54 |
55 | //export traces to custom express server running on port 4000
56 | const traceExporter = new OTLPTraceExporter({
57 | url: 'http://localhost:4000/', //export traces as http req to custom express server on port 400
58 | });
59 |
60 | //add exporter to provider / register provider
61 | provider.addSpanProcessor(new SimpleSpanProcessor(traceExporter));
62 | provider.register();
63 |
64 | // --- EXPRESS SERVER / SOCKET SETUP --- //
65 |
66 | //express configuration
67 | const express = require('express');
68 | const app = express();
69 | app.use(express.json());
70 | app.use(express.urlencoded({ extended: true }));
71 |
72 | const otelController = require('./otelController'); //import middleware
73 |
74 | //custom express server running on port 4000 to send data to front end dashboard
75 | app.use('/', otelController.parseTrace, (req, res) => {
76 | if (res.locals.clientData.length > 0) io.emit('message', JSON.stringify(res.locals.clientData));
77 | res.sendStatus(200);
78 | });
79 |
80 | //start custom express server on port 4000
81 | const server = app
82 | .listen(4000, () => {
83 | console.log(`Custom trace listening server on port 4000`);
84 | })
85 | .on('error', function (err) {
86 | process.once('SIGUSR2', function () {
87 | process.kill(process.pid, 'SIGUSR2');
88 | });
89 | process.on('SIGINT', function () {
90 | // this is only called on ctrl+c, not restart
91 | process.kill(process.pid, 'SIGINT');
92 | });
93 | });
94 |
95 | //create socket running on top of express server + enable cors
96 | const io = require('socket.io')(server, {
97 | cors: {
98 | origin: 'http://localhost:3000',
99 | credentials: true,
100 | },
101 | });
102 |
103 | // // -- TESTING -- //
104 | // // --- MONGOOSE SETUP (FOR TESTING) --- //
105 | const myURI = process.env.mongoURI;
106 |
107 | // using older version of mongoose, so need to set strictQuery or else get warning
108 | mongoose.set('strictQuery', true);
109 |
110 | // TODO: Remove the below mongoose test code for production build
111 | // connection to mongoDB using mongoose + test schema
112 | mongoose
113 | .connect(myURI, {
114 | useNewUrlParser: true,
115 | useUnifiedTopology: true,
116 | dbName: 'Movies',
117 | })
118 | .then(() => console.log('Connected to MongoDB'))
119 | .catch((err) => console.log('Error connecting to DB: ', err));
120 |
121 | mongoose.models = {};
122 |
123 | // deconstructed mongoose.Schema and mongoose.model
124 | const { Schema, model } = mongoose;
125 |
126 | // schema for movies
127 | const movieSchema = new Schema({
128 | title: {
129 | type: String,
130 | required: true,
131 | },
132 | watched: {
133 | type: Boolean,
134 | required: true,
135 | },
136 | });
137 |
138 | // model for movies using movieSchema
139 | const Movie = model('Movies', movieSchema, 'Movies');
140 |
141 | // --- PG SETUP (FOR TESTING) --- //
142 | const pool = new Pool({
143 | connectionString: process.env.pgURI,
144 | });
145 |
146 | module.exports = { Movie, pool };
147 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "paths": {
18 | "@/*": ["./*"]
19 | }
20 | },
21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "pages/functions/tooltip.ts"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | export interface DataType {
2 | spanId: string;
3 | traceId: string;
4 | startTime: number;
5 | duration: number;
6 | contentLength: number | null;
7 | statusCode: number;
8 | endPoint: string;
9 | requestType: string;
10 | requestMethod: string;
11 | }
12 |
--------------------------------------------------------------------------------