Skip to content

Commit 14f8df4

Browse files
committed
Initial version of a frontend service.
This uses Faro to instrument telemetry from the Browser app. RSBuild for React. Things have changed a little since the last time I wrote a frontend app! Signed-off-by: Heds Simons <hedley.simons@grafana.com>
1 parent a94034b commit 14f8df4

File tree

23 files changed

+2741
-20
lines changed

23 files changed

+2741
-20
lines changed

.github/workflows/publish-and-deploy-images.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ jobs:
3939
context: source
4040
service: mythical-beasts-recorder
4141
setup-qemu: true
42+
- file: source/mythical-beasts-frontend/Dockerfile
43+
tag_suffix: mythical-beasts-frontend
44+
context: source/mythical-beasts-frontend
45+
service: mythical-beasts-frontend
46+
setup-qemu: true
4247

4348
steps:
4449
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This readme has the following sections:
1818
- [Tempo](#tempo)
1919
- [Pyroscope](#pyroscope)
2020
- [k6](#k6)
21+
- [Faro Web SDK](#faro-web-sdk)
2122
- [Beyla](#beyla)
2223
- [Grafana Alloy](#grafana-alloy)
2324
- [Metrics Generation](#metrics-generation)
@@ -55,6 +56,7 @@ The demos from this series were based on the application and code in this reposi
5556
* A REST API server that receives requests and utilises a Database for storing/retrieving data for those requests.
5657
* A recorder service for storing messages to an AMQP bus.
5758
* A Postgres Database for storing/retrieving data from.
59+
* A React-based frontend web interface for managing the REST API server.
5860
* k6 service running a load test against the above application.
5961
* Tempo service for storing and querying trace information.
6062
* Loki service for storing and querying log information.
@@ -89,10 +91,11 @@ To execute the environment and login:
8991
```
9092
- "3123:3000"
9193
```
92-
3. Navigate to the [MLT dashboard](http://localhost:3000/d/ed4f4709-4d3b-48fd-a311-a036b85dbd5b/mlt-dashboard?orgId=1&refresh=5s).
93-
4. Explore the data sources using the [Grafana Explorer](http://localhost:3000/explore?orgId=1&left=%7B%22datasource%22:%22Mimir%22,%22queries%22:%5B%7B%22refId%22:%22A%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D).
94+
3. Access the service frontend at http://localhost:3001/ to interact with the data management interface. This will produce Faro session telemetry.
95+
4. Navigate to the [MLT dashboard](http://localhost:3000/d/ed4f4709-4d3b-48fd-a311-a036b85dbd5b/mlt-dashboard?orgId=1&refresh=5s).
96+
5. Explore the data sources using the [Grafana Explorer](http://localhost:3000/explore?orgId=1&left=%7B%22datasource%22:%22Mimir%22,%22queries%22:%5B%7B%22refId%22:%22A%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D).
9497

95-
The [pre-provisioned dashboard](grafana/definitions/mlt.json) demonstrates a [RED (Rate, Error, Duration)](https://grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services/) overview of the microservice application, where almost all metrics are being generated via trace spans. The dashboard also provides an example of logging.
98+
The [pre-provisioned dashboards](grafana/definitions/mlt.json) demonstrates a [RED (Rate, Error, Duration)](https://grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services/) overview of the microservice application, where almost all metrics are being generated via trace spans. The dashboard also provides an example of logging.
9699

97100
[Data links](https://grafana.com/docs/grafana/latest/panels-visualizations/configure-data-links/), [exemplars](https://grafana.com/docs/grafana-cloud/data-configuration/traces/exemplars/), and logs are utilized to allow jumping from the dashboard to a Grafana Explore page to observe traces, metrics, and logs in more detail.
98101

@@ -199,6 +202,15 @@ k6 can run one of more VU (Virtual Users) concurrently, to simulate parallel loa
199202

200203
k6 will generate [metrics](https://k6.io/docs/using-k6/metrics/) about the tests that it carries out, and will send these to the running Mimir instance. These metrics can then be used to determine the latencies of endpoints, number of errors occurring, etc. The official Grafana dashboard for k6 is included, and once the sandbox is running, may be found [here](http://localhost:3000/d/01npcT44k/official-k6-test-result?orgId=1&refresh=10s).
201204

205+
### Faro Web SDK
206+
207+
The Faro Web SDK is a Grafana component that collects observability telemetry from within a web application and emits it to a relevant collector. This can be the likes of Grafana Alloy or directly to Grafana Cloud's Frontend endpoint. For more details about Faro Web SDK, read the [documentation](https://github.com/grafana/faro-web-sdk).
208+
209+
In the Intro to MLTP repository, Faro instruments the frontend service described in the `mythical-frontend` section of the [`docker-compose.yml`](docker-compose.yml) manifest, with source in the [`source/mythical-beasts-frontend`](source/mythical-beasts-frontend) directory.
210+
Note that this service is optional, and is only currently available in the local Alloy-based Docker Compose manifest. The OpenTelemetry Collector does not receive Faro telemetry.
211+
212+
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.
213+
202214
### Beyla
203215

204216
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/).

alloy/config.alloy

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ loki.process "mythical" {
150150
forward_to = [loki.write.mythical.receiver]
151151
}
152152

153+
// Write out to the appropriate Loki instance.
153154
loki.write "mythical" {
154155
// Output the Loki log to the local Loki instance.
155156
endpoint {
@@ -163,6 +164,35 @@ loki.write "mythical" {
163164
}
164165
}
165166

167+
// The Loki processor for Faro frontend logs
168+
loki.process "faro" {
169+
// Add labels to identify frontend logs
170+
stage.static_labels {
171+
values = {
172+
job = "mythical-frontend",
173+
group = "mythical",
174+
service_name = "mythical-frontend",
175+
}
176+
}
177+
178+
// Forward to the Faro Loki writer for output
179+
forward_to = [loki.write.mythical.receiver]
180+
}
181+
182+
183+
/*loki.write "faro" {
184+
// Output the Faro logs to the local Loki instance
185+
endpoint {
186+
url = json_path(local.file.endpoints.content, ".logs.url")[0]
187+
188+
// The basic auth credentials for the Loki instance
189+
basic_auth {
190+
username = json_path(local.file.endpoints.content, ".logs.basicAuth.username")[0]
191+
password = json_path(local.file.endpoints.content, ".logs.basicAuth.password")[0]
192+
}
193+
}
194+
}*/
195+
166196
///////////////////////////////////////////////////////////////////////////////
167197
// Tracing
168198

@@ -206,6 +236,26 @@ otelcol.receiver.otlp "otlp_receiver" {
206236
}
207237
}
208238

239+
///////////////////////////////////////////////////////////////////////////////
240+
// Faro (Frontend Observability)
241+
242+
// The Faro receiver is used to ingest frontend telemetry data from Grafana Faro SDK.
243+
// This includes traces, logs, measurements, and web vitals from browser applications.
244+
faro.receiver "frontend" {
245+
// Listen for Faro data on port 12350 (custom port to avoid conflicts)
246+
server {
247+
listen_address = "0.0.0.0"
248+
listen_port = 12350
249+
cors_allowed_origins = ["*"]
250+
}
251+
252+
// Forward all received data to the appropriate processors
253+
output {
254+
logs = [loki.process.faro.receiver]
255+
traces = [otelcol.processor.batch.default.input]
256+
}
257+
}
258+
209259
// The OpenTelemetry batch processor collects trace spans until a batch size or timeout is met, before sending those
210260
// spans onto another target. This processor is labeled 'default'.
211261
otelcol.processor.batch "default" {

docker-compose.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ services:
99
alloy:
1010
image: grafana/alloy:v1.9.1
1111
ports:
12+
- "12350:12350"
1213
- "12347:12345"
1314
- "12348:12348"
1415
- "6832:6832"
@@ -143,6 +144,24 @@ services:
143144
- OTEL_EXPORTER_OTLP_TRACES_INSECURE=true
144145
- OTEL_RESOURCE_ATTRIBUTES=ip=1.2.3.5
145146

147+
# React frontend for the mythical beasts management system
148+
mythical-frontend:
149+
#build:
150+
# context: ./source/mythical-beasts-frontend
151+
# dockerfile: Dockerfile
152+
# args:
153+
# - REACT_APP_API_URL=/api
154+
# - REACT_APP_ALLOY_ENDPOINT=http://localhost:12350/collect
155+
image: grafana/intro-to-mltp:mythical-beasts-frontend-latest
156+
restart: always
157+
depends_on:
158+
mythical-server:
159+
condition: service_started
160+
alloy:
161+
condition: service_started
162+
ports:
163+
- "3001:80"
164+
146165
# The Tempo service stores traces send to it by Grafana Alloy, and takes
147166
# queries from Grafana to visualise those traces.
148167
tempo:
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Dependencies
2+
/node_modules
3+
/.pnp
4+
.pnp.js
5+
6+
# Testing
7+
/coverage
8+
9+
# Production
10+
/build
11+
12+
# Misc
13+
.DS_Store
14+
.env.local
15+
.env.development.local
16+
.env.test.local
17+
.env.production.local
18+
19+
# Logs
20+
npm-debug.log*
21+
yarn-debug.log*
22+
yarn-error.log*
23+
24+
# IDE
25+
.vscode/
26+
.idea/
27+
*.swp
28+
*.swo
29+
30+
# OS
31+
Thumbs.db
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Build stage
2+
FROM node:18-alpine AS build
3+
4+
# Accept build arguments
5+
ARG REACT_APP_API_URL
6+
ARG REACT_APP_ALLOY_ENDPOINT
7+
8+
# Set as environment variables for the build process
9+
ENV REACT_APP_API_URL=$REACT_APP_API_URL
10+
ENV REACT_APP_ALLOY_ENDPOINT=$REACT_APP_ALLOY_ENDPOINT
11+
12+
WORKDIR /app
13+
14+
# Copy package files
15+
COPY package*.json ./
16+
17+
# Install all dependencies (including dev dependencies needed for build)
18+
RUN npm ci
19+
20+
# Copy source code
21+
COPY . .
22+
23+
# Build the React app with rsbuild
24+
RUN npm run build
25+
26+
# Production stage
27+
FROM nginx:alpine
28+
29+
# Copy custom nginx config
30+
COPY nginx.conf /etc/nginx/nginx.conf
31+
32+
# Copy built app from build stage
33+
COPY --from=build /app/build /usr/share/nginx/html
34+
35+
# Expose port 80
36+
EXPOSE 80
37+
38+
# Start nginx
39+
CMD ["nginx", "-g", "daemon off;"]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
events {
2+
worker_connections 1024;
3+
}
4+
5+
http {
6+
include /etc/nginx/mime.types;
7+
default_type application/octet-stream;
8+
9+
# Logging
10+
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
11+
'$status $body_bytes_sent "$http_referer" '
12+
'"$http_user_agent" "$http_x_forwarded_for"';
13+
14+
access_log /var/log/nginx/access.log main;
15+
error_log /var/log/nginx/error.log;
16+
17+
# Gzip compression
18+
gzip on;
19+
gzip_vary on;
20+
gzip_min_length 1024;
21+
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
22+
23+
# Server configuration
24+
server {
25+
listen 80;
26+
server_name localhost;
27+
root /usr/share/nginx/html;
28+
index index.html;
29+
30+
# Security headers
31+
add_header X-Frame-Options "SAMEORIGIN" always;
32+
add_header X-Content-Type-Options "nosniff" always;
33+
add_header X-XSS-Protection "1; mode=block" always;
34+
35+
# Handle React Router (client-side routing)
36+
location / {
37+
try_files $uri $uri/ /index.html;
38+
}
39+
40+
# Cache static assets
41+
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
42+
expires 1y;
43+
add_header Cache-Control "public, immutable";
44+
}
45+
46+
# API proxy to backend - proxy API requests to the mythical-server
47+
location /api/ {
48+
# Remove /api prefix and proxy to backend
49+
rewrite ^/api/(.*)$ /$1 break;
50+
proxy_pass http://mythical-server:4000;
51+
proxy_set_header Host $host;
52+
proxy_set_header X-Real-IP $remote_addr;
53+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
54+
proxy_set_header X-Forwarded-Proto $scheme;
55+
56+
# CORS headers for direct API access via nginx
57+
add_header Access-Control-Allow-Origin "http://localhost:3001" always;
58+
add_header Access-Control-Allow-Methods "GET, POST, DELETE, OPTIONS" always;
59+
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
60+
add_header Access-Control-Allow-Credentials "true" always;
61+
62+
# Handle preflight requests
63+
if ($request_method = 'OPTIONS') {
64+
add_header Access-Control-Allow-Origin "http://localhost:3001";
65+
add_header Access-Control-Allow-Methods "GET, POST, DELETE, OPTIONS";
66+
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
67+
add_header Access-Control-Allow-Credentials "true";
68+
add_header Content-Length 0;
69+
add_header Content-Type text/plain;
70+
return 204;
71+
}
72+
}
73+
74+
# Health check endpoint
75+
location /health {
76+
access_log off;
77+
return 200 "healthy\n";
78+
add_header Content-Type text/plain;
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)