Skip to content

Commit a51d0b7

Browse files
committed
Add Grafana Cloud Frontend functionality to the Frontend app.
This will allow the use of a Frontend endpoint to be used, to use Grafana Frontend functionality in Cloud. The `mythical-server` service has been updated to provide configuration for the Faro Web SDK to ensure that either a local instance of Alloy is used for data collection, or Grafana Cloud Frontend. Signed-off-by: Heds Simons <hedley.simons@grafana.com>
1 parent 14f8df4 commit a51d0b7

File tree

10 files changed

+149
-43
lines changed

10 files changed

+149
-43
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,14 @@ Note that this service is optional, and is only currently available in the local
211211

212212
Faro is designed to continue to propagate data across frontend sessions to backend server infrastructure, and as such includes the ability to send relevant state information in headers. For traces, this is based open the OpenTelemetry Tracing Specification, and utilises the `tracestate` and `traceparent` headers to propagate data. In the case of this example, this will show traces starting in the `mythical-frontend` service.
213213

214+
The default setup is to send Faro data to the local Alloy instance, which will count it as a propagated trace that will include the `mythical-server` service as part of the trace.
215+
216+
For Grafana Cloud, using the [`docker-compose-cloud.yml`](docker-compose-cloud.yml) manifest, you can instead use Grafana Frontend to show session data, as well as statistics on user browser data such as time to first byte, etc. To do so, change the following environment variables in the `mythical-server` configuration:
217+
* `FARO_ENDPOINT` to the URL specified in an initialised Grafana Frontend application project, notably the `url` field in the `Settings -> Web SDK Config` section.
218+
* `USE_GRAFANA_CLOUD` to `true`.
219+
This will allow you to send Faro data to Grafana Cloud, and see linked traces and logs as part of Grafana Frontend.
220+
221+
214222
### Beyla
215223

216224
Beyla is an eBPF-based tool for generating metrics and trace data without the need for application instrumentation. For more details about Beyla, read the [documentation](https://grafana.com/docs/grafana-cloud/monitor-applications/beyla/).
@@ -333,6 +341,8 @@ This demo can be run against Grafana Cloud by configuring the `alloy/endpoints-c
333341

334342
The Grafana Alloy will send all the signals to the Grafana Cloud stack specified in the `alloy/endpoints-cloud.json` file.
335343

344+
*Note:* See the [`Faro Web SDK`](#faro-web-sdk) for instructions on how to configure the right URL endpoint to send Faro data to Grafana Frontend. It is *vital* to note that you must configure your browser to disable the same-origin policies to allow the local running of a web-server that will send to Grafana Frontend (and to include `localhost` in your allowed domains). For example, in Chrome you can achieve this with the `--disable-web-security` flag.
345+
336346
## Using the OpenTelemetry Collector
337347

338348
You can also use an alternative environment that uses the OpenTelemetry Collector in place of Grafana Alloy.

docker-compose-cloud.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ services:
1111
ports:
1212
- "12347:12345"
1313
- "12348:12348"
14+
- "12350:12350"
1415
- "6832:6832"
1516
- "55679:55679"
1617
- "4317:4317"
@@ -75,6 +76,25 @@ services:
7576
- OTEL_EXPORTER_OTLP_TRACES_INSECURE=true
7677
- OTEL_RESOURCE_ATTRIBUTES=ip=1.2.3.4
7778

79+
# React frontend for the mythical beasts management system
80+
mythical-frontend:
81+
#build:
82+
# context: ./source/mythical-beasts-frontend
83+
# dockerfile: Dockerfile
84+
# args:
85+
# - REACT_APP_API_URL=/api
86+
# By default, this will try and send to Alloy instead of Grafana Cloud Frontend.
87+
image: grafana/intro-to-mltp:mythical-beasts-frontend-latest
88+
restart: always
89+
depends_on:
90+
mythical-server:
91+
condition: service_started
92+
alloy:
93+
condition: service_started
94+
ports:
95+
- "3001:80"
96+
97+
7898
# The API server microservice.
7999
# It writes logs directly to the Loki service, exposes metrics for the Prometheus
80100
# service and sends traces to the Grafana Alloy instance.
@@ -100,6 +120,11 @@ services:
100120
- PROFILE_COLLECTOR_PORT=4040
101121
- OTEL_EXPORTER_OTLP_TRACES_INSECURE=true
102122
- OTEL_RESOURCE_ATTRIBUTES=ip=1.2.3.5
123+
# By default, this will try and send Faro data to the local Alloy instance.
124+
# For Grafana Cloud, set FARO_ENDPOINT to your Grafana Cloud Frontend endpoint, and set
125+
# USE_GRAFANA_CLOUD to true.
126+
- FARO_ENDPOINT=http://localhost:12350/collect
127+
- USE_GRAFANA_CLOUD=false
103128

104129
# A microservice that consumes requests from the mythical-queue
105130
mythical-recorder:

docker-compose.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ services:
119119
- PROFILE_COLLECTOR_PORT=4040
120120
- OTEL_EXPORTER_OTLP_TRACES_INSECURE=true
121121
- OTEL_RESOURCE_ATTRIBUTES=ip=1.2.3.5
122+
# Faro configuration for the frontend, sends to the local Alloy instance.
123+
# You can change this to send to Grafana Cloud, but see the `docker-compose-cloud.yml` compose file instead
124+
# to send all data to Grafana Cloud.
125+
- FARO_ENDPOINT=http://localhost:12350/collect
126+
- USE_GRAFANA_CLOUD=false
122127

123128
# A microservice that consumes requests from the mythical-queue
124129
mythical-recorder:
@@ -151,7 +156,6 @@ services:
151156
# dockerfile: Dockerfile
152157
# args:
153158
# - REACT_APP_API_URL=/api
154-
# - REACT_APP_ALLOY_ENDPOINT=http://localhost:12350/collect
155159
image: grafana/intro-to-mltp:mythical-beasts-frontend-latest
156160
restart: always
157161
depends_on:

grafana/definitions/traces-in-dashboards.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
"uid": "mimir"
109109
},
110110
"editorMode": "code",
111-
"expr": "sum by (http_method)(rate(traces_spanmetrics_calls_total{service_name=\"mythical-server\",http_method=~\"${httpMethod}\"}[1m]))",
111+
"expr": "sum by (http_method)(rate(traces_spanmetrics_calls_total{service=\"mythical-server\",http_method=~\"${httpMethod}\"}[1m]))",
112112
"legendFormat": "",
113113
"range": true,
114114
"refId": "A"
30.5 KB
Loading

source/mythical-beasts-frontend/Dockerfile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
# Build stage
22
FROM node:18-alpine AS build
33

4-
# Accept build arguments
4+
# Accept build arguments (only keep API URL for build-time configuration)
55
ARG REACT_APP_API_URL
6-
ARG REACT_APP_ALLOY_ENDPOINT
76

87
# Set as environment variables for the build process
98
ENV REACT_APP_API_URL=$REACT_APP_API_URL
10-
ENV REACT_APP_ALLOY_ENDPOINT=$REACT_APP_ALLOY_ENDPOINT
119

1210
WORKDIR /app
1311

source/mythical-beasts-frontend/rsbuild.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export default defineConfig({
3434
source: {
3535
define: {
3636
'process.env.REACT_APP_API_URL': JSON.stringify(process.env.REACT_APP_API_URL || '/api'),
37-
'process.env.REACT_APP_ALLOY_ENDPOINT': JSON.stringify(process.env.REACT_APP_ALLOY_ENDPOINT || 'http://localhost:12350/collect'),
3837
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
3938
},
4039
},

source/mythical-beasts-frontend/src/faro.js

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,94 @@ import { getWebInstrumentations, initializeFaro } from '@grafana/faro-web-sdk';
22
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
33

44
let faro = null;
5+
let configPromise = null;
56

6-
export const initFaro = () => {
7+
// Fetch configuration from backend API
8+
const fetchFaroConfig = async () => {
9+
try {
10+
// Use the same API base URL logic as the rest of the app
11+
const isProduction = process.env.NODE_ENV === 'production';
12+
const isDockerizedFrontend = window.location.hostname === 'localhost' && window.location.port === '3001';
13+
14+
let baseUrl;
15+
if (isProduction && isDockerizedFrontend) {
16+
baseUrl = '/api'; // Use nginx proxy
17+
} else {
18+
baseUrl = 'http://localhost:4000'; // Direct API access
19+
}
20+
21+
const response = await fetch(`${baseUrl}/config`);
22+
if (!response.ok) {
23+
throw new Error(`HTTP error! status: ${response.status}`);
24+
}
25+
26+
const config = await response.json();
27+
console.log('🔧 Fetched Faro config from backend:', config);
28+
return config;
29+
} catch (error) {
30+
console.warn('⚠️ Failed to fetch Faro config from backend, using defaults:', error);
31+
// Return default configuration if API call fails
32+
return {
33+
faroUrl: 'http://localhost:12350/collect',
34+
useGrafanaCloud: false,
35+
environment: 'development',
36+
version: '1.0.0',
37+
appName: 'mythical-frontend'
38+
};
39+
}
40+
};
41+
42+
export const initFaro = async () => {
743
// Return early if already initialized
844
if (faro) {
945
return faro;
1046
}
1147

48+
// Use cached config promise if available
49+
if (!configPromise) {
50+
configPromise = fetchFaroConfig();
51+
}
52+
1253
try {
13-
// Get the Alloy endpoint from environment
14-
const alloyEndpoint = process.env.REACT_APP_ALLOY_ENDPOINT || 'http://localhost:12350/collect';
54+
// Wait for configuration from backend
55+
const config = await configPromise;
1556

16-
console.log('🔍 Initializing Faro with endpoint:', alloyEndpoint);
57+
console.log('🔍 Faro URL:', config.faroUrl);
58+
console.log('🔍 Using Grafana Cloud:', config.useGrafanaCloud);
59+
console.log('🔍 CORS Proxy:', config.useGrafanaCloud ? 'Enabled (via localhost:8080)' : 'Not needed');
1760

18-
// Initialisation with a average configuration.
61+
// Standard Faro initialization
1962
faro = initializeFaro({
20-
// This is the endpoint to send telemetry to. In this case, we're sending to Alloy.
21-
url: alloyEndpoint,
63+
url: config.faroUrl,
64+
2265
// The application details will set relevant resources attributes.
2366
app: {
24-
name: 'mythical-frontend',
25-
version: '1.0.0',
26-
environment: 'development',
67+
name: config.appName,
68+
version: config.version,
69+
environment: config.environment,
2770
},
2871

29-
// Basic session tracking
72+
// Enhanced session tracking for Grafana Cloud
3073
sessionTracking: {
3174
enabled: true,
32-
persistent: false, // Simplified to avoid storage issues
75+
persistent: config.useGrafanaCloud, // Use persistent sessions for cloud
3376
},
3477

35-
// Configure batching with longer timeouts to be a bit more forgiving on slower machines.
78+
// Configure batching with appropriate timeouts
3679
batching: {
3780
enabled: true,
38-
sendTimeout: 5000, // Longer timeout to prevent connection issues
81+
sendTimeout: config.useGrafanaCloud ? 10000 : 5000, // Longer timeout for cloud
82+
itemLimit: config.useGrafanaCloud ? 50 : 25, // More items per batch for cloud
3983
},
4084

41-
// Basic instrumentation.
85+
// Enhanced instrumentation for better observability
4286
instrumentations: [
4387
...getWebInstrumentations({
44-
captureConsole: false, // Disable console capture to reduce noisey output. Set to true to capture:
45-
captureConsoleDisabledLevels: ['debug', 'log'],
88+
captureConsole: true,
89+
captureConsoleDisabledLevels: ['debug'], // Capture more for cloud
4690
}),
4791

48-
// Add tracing instrumentation with simple configuration.
92+
// Add tracing instrumentation
4993
new TracingInstrumentation({
5094
instrumentationOptions: {
5195
// Only trace API calls to our backend
@@ -59,7 +103,6 @@ export const initFaro = () => {
59103

60104
// Some basic error handling.
61105
beforeSend: (event) => {
62-
// Simple pass-through with error handling
63106
try {
64107
return event;
65108
} catch (error) {
@@ -74,9 +117,14 @@ export const initFaro = () => {
74117
// Send a simple test event
75118
setTimeout(() => {
76119
try {
77-
faro.api.pushLog(['Faro SDK test log'], {
120+
faro.api.pushLog(['Faro SDK initialized successfully'], {
78121
level: 'info',
79-
context: { source: 'faro-init' }
122+
context: {
123+
source: 'faro-init',
124+
endpoint: config.faroUrl,
125+
useGrafanaCloud: config.useGrafanaCloud,
126+
corsProxy: config.useGrafanaCloud ? 'localhost:8080' : 'none'
127+
}
80128
});
81129
} catch (error) {
82130
console.warn('Failed to send test log:', error);
@@ -94,13 +142,13 @@ export const initFaro = () => {
94142
pushLog: () => {},
95143
pushError: () => {},
96144
pushMeasurement: (measurement, operation) => {
97-
// If there's an operation function, just call it directly
98145
if (typeof operation === 'function') {
99146
return operation();
100147
}
101148
return Promise.resolve();
102149
},
103150
getTraceContext: () => null,
151+
getOTEL: () => null,
104152
}
105153
};
106154

source/mythical-beasts-frontend/src/index.js

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@ import './index.css';
44
import App from './App';
55
import { initFaro } from './faro';
66

7-
// Always initialise Faro first, to ensure that it is available for the React app.
8-
console.log('🔍 Initializing Faro SDK before React app...');
9-
try {
10-
initFaro();
11-
console.log('✅ Faro SDK initialized successfully');
12-
} catch (error) {
13-
console.warn('⚠️ Faro SDK failed to initialize, continuing without observability:', error);
14-
}
7+
// Initialize Faro SDK asynchronously
8+
const initializeApp = async () => {
9+
try {
10+
await initFaro();
11+
console.log('✅ Faro SDK initialized successfully');
12+
} catch (error) {
13+
console.warn('⚠️ Faro SDK failed to initialize, continuing without observability:', error);
14+
}
1515

16-
// Render the app!
17-
const root = ReactDOM.createRoot(document.getElementById('root'));
18-
root.render(
19-
<React.StrictMode>
20-
<App />
21-
</React.StrictMode>
22-
);
16+
// Render the app regardless of Faro initialization status
17+
const root = ReactDOM.createRoot(document.getElementById('root'));
18+
root.render(
19+
<React.StrictMode>
20+
<App />
21+
</React.StrictMode>
22+
);
23+
};
24+
25+
// Start the app
26+
initializeApp();

source/mythical-beasts-server/index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,24 @@ const logUtils = require('./logging')('mythical-server', 'server');
126126
});
127127
};
128128

129+
// Configuration endpoint for frontend settings.
130+
// This retrieves the correct endpoint to use for Faro data.
131+
app.get('/config', async (req, res) => {
132+
try {
133+
const config = {
134+
faroUrl: process.env.FARO_ENDPOINT || 'http://localhost:12350/collect',
135+
useGrafanaCloud: process.env.USE_GRAFANA_CLOUD === 'true',
136+
environment: process.env.NODE_ENV || 'development',
137+
version: '1.0.0',
138+
appName: 'mythical-frontend'
139+
};
140+
141+
res.json(config);
142+
} catch (error) {
143+
res.status(500).json({ error: 'Failed to get configuration' });
144+
}
145+
});
146+
129147
// Metrics endpoint handler (for Prometheus scraping)
130148
app.get('/metrics', async (req, res) => {
131149
res.set('Content-Type', register.contentType);

0 commit comments

Comments
 (0)