Skip to content

Commit

Permalink
feat(*): add more telemetry and configure tempo properly to expose sp…
Browse files Browse the repository at this point in the history
…ans metrics
  • Loading branch information
joaquin-diaz committed Jul 26, 2024
1 parent a712436 commit 6c04ab8
Show file tree
Hide file tree
Showing 19 changed files with 389 additions and 76 deletions.
4 changes: 3 additions & 1 deletion backend/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ services:
restart: always

grafana:
image: grafana/otel-lgtm
build:
context: .
dockerfile: grafana.Dockerfile
ports:
- '3000:3000'
- '4317:4317'
Expand Down
5 changes: 5 additions & 0 deletions backend/grafana.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM grafana/otel-lgtm

COPY tempo-config.yaml .

CMD /otel-lgtm/run-all.sh
44 changes: 44 additions & 0 deletions backend/tempo-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
server:
http_listen_port: 3200
grpc_listen_port: 9096

distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: "localhost:4417"
http:
endpoint: "localhost:4418"

storage:
trace:
backend: local
wal:
path: /tmp/tempo/wal
local:
path: /tmp/tempo/blocks

metrics_generator:
registry:
# A list of labels that will be added to all generated metrics.
external_labels:
source: tempo
processor:
span_metrics:
enable_target_info: true
dimensions: ['react_client.cold_start', 'react_client.page_name', 'react_client.product_id']
local_blocks:
filter_server_spans: false
traces_storage:
path: /tmp/tempo/generator/traces
storage:
path: /tmp/tempo/generator/wal
remote_write:
- url: http://localhost:9090/api/v1/write
send_exemplars: true

overrides:
defaults:
metrics_generator:
processors: [service-graphs, span-metrics, local-blocks]
3 changes: 3 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<link rel="preload" data-rocket-preload="" as="style" href="https://fonts.googleapis.com/css2?family=Open+Sans+Condensed:ital,wght@0,300;0,700;1,300&amp;family=Open+Sans+Condensed:ital,wght@0,300;0,700;1,300&amp;family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;0,800;1,300;1,400;1,600;1,700;1,800&amp;family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&amp;family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&amp;family=Source+Serif+Pro:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700;1,900&amp;display=swap">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<script>
window.coldStart = true;
</script>
</head>
<body>
<div id="root"></div>
Expand Down
1 change: 0 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.52.1",
"@opentelemetry/exporter-prometheus": "^0.52.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
"@opentelemetry/instrumentation": "^0.52.1",
"@opentelemetry/instrumentation-document-load": "^0.39.0",
Expand Down
15 changes: 9 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import ProductDetails from "./pages/ProductDetails";
import SpansProvider from "./components/SpansProvider/SpansProvider";

const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products/:productId" element={<ProductDetails />} />
</Routes>
</BrowserRouter>
<SpansProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products/:productId" element={<ProductDetails />} />
</Routes>
</BrowserRouter>
</SpansProvider>
);
};

Expand Down
56 changes: 56 additions & 0 deletions frontend/src/components/Page/Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { FC, PropsWithChildren, useEffect, useRef } from "react";
import { Attributes } from "@opentelemetry/api";
import useTrackPageView from "../../hooks/useTrackPageView";
import useSpansContext from "../SpansProvider/hooks/useSpansContext";

type PageProps = {
isLoading?: boolean;
instrumentation: {
pageName: string;
attributes?: Attributes;
};
};

const Page: FC<PropsWithChildren<PageProps>> = ({
children,
instrumentation,
isLoading,
}) => {
const wasColdStart = useRef(window.coldStart);

const { pageName, attributes } = instrumentation;
const { endSpan, getOrCreateSpan } = useSpansContext();

getOrCreateSpan("Page Ready", {
startTime: wasColdStart.current
? performance.timeOrigin
: new Date().getTime(),
attributes: {
"react_client.cold_start": wasColdStart.current,
"react_client.page_name": pageName,
...attributes,
},
});

useEffect(() => {
if (window.coldStart) {
window.coldStart = false;
}
}, []);

useEffect(() => {
if (isLoading === undefined || !isLoading) {
endSpan("Page Ready");
}
}, [endSpan, isLoading]);

useTrackPageView(pageName, attributes);

if (isLoading) {
return "Loading";
}

return <>{children}</>;
};

export default Page;
3 changes: 3 additions & 0 deletions frontend/src/components/Page/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Page from "./Page";

export default Page;
51 changes: 51 additions & 0 deletions frontend/src/components/SpansProvider/SpansProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { SpansContext } from "./hooks/useSpansContext";
import { FC, PropsWithChildren, useCallback, useRef } from "react";
import { context, Span, SpanOptions, trace } from "@opentelemetry/api";
import { SpanName } from "./constants/spans";

const SpansProvider: FC<PropsWithChildren> = ({ children }) => {
const spansRef = useRef<Map<SpanName, Span>>(new Map());

const getOrCreateSpan = useCallback(
(name: SpanName, options?: SpanOptions): [Span, boolean] => {
if (spansRef.current.has(name)) {
return [spansRef.current.get(name)!, false];
}

const tracer = trace.getTracer("react-client");
const span = tracer.startSpan(name, options);

spansRef.current.set(name, span);

return [span, true];
},
[],
);

const withSpanContext = useCallback(
(name: SpanName, callback: () => void) => {
const [span] = getOrCreateSpan(name);
context.with(trace.setSpan(context.active(), span), callback);
},
[getOrCreateSpan],
);

const endSpan = useCallback((name: SpanName) => {
const span = spansRef.current.get(name);

if (span) {
span.end();
spansRef.current.delete(name);
}
}, []);

return (
<SpansContext.Provider
value={{ getOrCreateSpan, withSpanContext, endSpan }}
>
{children}
</SpansContext.Provider>
);
};

export default SpansProvider;
3 changes: 3 additions & 0 deletions frontend/src/components/SpansProvider/constants/spans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type SpanName = "Page Ready" | "Purchase Flow";

export { type SpanName };
28 changes: 28 additions & 0 deletions frontend/src/components/SpansProvider/hooks/useSpansContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createContext, useContext } from "react";
import { Span, SpanOptions } from "@opentelemetry/api";
import { SpanName } from "../constants/spans";

type SpansContextValue = {
getOrCreateSpan: (name: SpanName, options?: SpanOptions) => [Span, boolean];
withSpanContext: (name: SpanName, callback: () => void) => void;
endSpan: (name: SpanName) => void;
};

const SpansContext = createContext<SpansContextValue>({
getOrCreateSpan: () => {
throw new Error("ActiveSpanContextValue not provided");
},
withSpanContext: () => {
throw new Error("ActiveSpanContextValue not provided");
},
endSpan: () => {
throw new Error("ActiveSpanContextValue not provided");
},
});

const useSpansContext = () => {
return useContext(SpansContext);
};

export default useSpansContext;
export { SpansContext };
41 changes: 41 additions & 0 deletions frontend/src/hooks/useMeasureReadiness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useEffect, useRef } from "react";
import { Attributes, trace } from "@opentelemetry/api";

type UseMeasureReadinessArgs = {
pageName: string;
areNetworkRequestsDone?: boolean;
};

const useMeasureReadiness = (
{ pageName, areNetworkRequestsDone }: UseMeasureReadinessArgs,
extraAttributes: Attributes = {},
) => {
const now = useRef<number>(
window.coldStart ? performance.timeOrigin : new Date().getTime(),
);

useEffect(() => {
if (window.coldStart) {
window.coldStart = false;
}
}, []);

useEffect(() => {
const tracer = trace.getTracer("react-client");

if (areNetworkRequestsDone === undefined || areNetworkRequestsDone) {
const span = tracer.startSpan("Page Ready", {
startTime: now.current,
});

span.setAttributes({
page_name: pageName,
...extraAttributes,
});

span.end();
}
}, [areNetworkRequestsDone]);
};

export default useMeasureReadiness;
24 changes: 24 additions & 0 deletions frontend/src/hooks/useTrackPageView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Attributes, metrics } from "@opentelemetry/api";
import { useEffect } from "react";

const useTrackPageView = (
pageName: string,
extraAttributes: Attributes = {},
) => {
useEffect(() => {
const meter = metrics.getMeter("react-client");

const counter = meter.createCounter("react_client_page_view", {
description: "Number of views for a page",
});

counter.add(1, {
"react_client.page_name": pageName,
...extraAttributes,
});
// Make sure this only runs once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};

export default useTrackPageView;
15 changes: 5 additions & 10 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import {setupOTelSDK} from "./otel.ts";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { setupOTelSDK } from "./otel.ts";

setupOTelSDK();

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
Loading

0 comments on commit 6c04ab8

Please sign in to comment.