Skip to content

Initial version of a frontend service. #333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/publish-and-deploy-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ jobs:
context: source
service: mythical-beasts-recorder
setup-qemu: true
- file: source/mythical-beasts-frontend/Dockerfile
tag_suffix: mythical-beasts-frontend
context: source/mythical-beasts-frontend
service: mythical-beasts-frontend
setup-qemu: true

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This readme has the following sections:
- [Tempo](#tempo)
- [Pyroscope](#pyroscope)
- [k6](#k6)
- [Faro Web SDK](#faro-web-sdk)
- [Beyla](#beyla)
- [Grafana Alloy](#grafana-alloy)
- [Metrics Generation](#metrics-generation)
Expand Down Expand Up @@ -55,6 +56,7 @@ The demos from this series were based on the application and code in this reposi
* A REST API server that receives requests and utilises a Database for storing/retrieving data for those requests.
* A recorder service for storing messages to an AMQP bus.
* A Postgres Database for storing/retrieving data from.
* A React-based frontend web interface for managing the REST API server.
* k6 service running a load test against the above application.
* Tempo service for storing and querying trace information.
* Loki service for storing and querying log information.
Expand Down Expand Up @@ -89,10 +91,11 @@ To execute the environment and login:
```
- "3123:3000"
```
3. Navigate to the [MLT dashboard](http://localhost:3000/d/ed4f4709-4d3b-48fd-a311-a036b85dbd5b/mlt-dashboard?orgId=1&refresh=5s).
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).
3. Access the service frontend at http://localhost:3001/ to interact with the data management interface. This will produce Faro session telemetry.
4. Navigate to the [MLT dashboard](http://localhost:3000/d/ed4f4709-4d3b-48fd-a311-a036b85dbd5b/mlt-dashboard?orgId=1&refresh=5s).
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).

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.
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.

[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.

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

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

### Faro Web SDK

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

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.
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.

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.

### Beyla

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/).
Expand Down
50 changes: 50 additions & 0 deletions alloy/config.alloy
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ loki.process "mythical" {
forward_to = [loki.write.mythical.receiver]
}

// Write out to the appropriate Loki instance.
loki.write "mythical" {
// Output the Loki log to the local Loki instance.
endpoint {
Expand All @@ -163,6 +164,35 @@ loki.write "mythical" {
}
}

// The Loki processor for Faro frontend logs
loki.process "faro" {
// Add labels to identify frontend logs
stage.static_labels {
values = {
job = "mythical-frontend",
group = "mythical",
service_name = "mythical-frontend",
}
}

// Forward to the Faro Loki writer for output
forward_to = [loki.write.mythical.receiver]
}


/*loki.write "faro" {
// Output the Faro logs to the local Loki instance
endpoint {
url = json_path(local.file.endpoints.content, ".logs.url")[0]

// The basic auth credentials for the Loki instance
basic_auth {
username = json_path(local.file.endpoints.content, ".logs.basicAuth.username")[0]
password = json_path(local.file.endpoints.content, ".logs.basicAuth.password")[0]
}
}
}*/

///////////////////////////////////////////////////////////////////////////////
// Tracing

Expand Down Expand Up @@ -206,6 +236,26 @@ otelcol.receiver.otlp "otlp_receiver" {
}
}

///////////////////////////////////////////////////////////////////////////////
// Faro (Frontend Observability)

// The Faro receiver is used to ingest frontend telemetry data from Grafana Faro SDK.
// This includes traces, logs, measurements, and web vitals from browser applications.
faro.receiver "frontend" {
// Listen for Faro data on port 12350 (custom port to avoid conflicts)
server {
listen_address = "0.0.0.0"
listen_port = 12350
cors_allowed_origins = ["*"]
}

// Forward all received data to the appropriate processors
output {
logs = [loki.process.faro.receiver]
traces = [otelcol.processor.batch.default.input]
}
}

// The OpenTelemetry batch processor collects trace spans until a batch size or timeout is met, before sending those
// spans onto another target. This processor is labeled 'default'.
otelcol.processor.batch "default" {
Expand Down
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ services:
alloy:
image: grafana/alloy:v1.9.1
ports:
- "12350:12350"
- "12347:12345"
- "12348:12348"
- "6832:6832"
Expand Down Expand Up @@ -143,6 +144,24 @@ services:
- OTEL_EXPORTER_OTLP_TRACES_INSECURE=true
- OTEL_RESOURCE_ATTRIBUTES=ip=1.2.3.5

# React frontend for the mythical beasts management system
mythical-frontend:
#build:
# context: ./source/mythical-beasts-frontend
# dockerfile: Dockerfile
# args:
# - REACT_APP_API_URL=/api
# - REACT_APP_ALLOY_ENDPOINT=http://localhost:12350/collect
image: grafana/intro-to-mltp:mythical-beasts-frontend-latest
restart: always
depends_on:
mythical-server:
condition: service_started
alloy:
condition: service_started
ports:
- "3001:80"

# The Tempo service stores traces send to it by Grafana Alloy, and takes
# queries from Grafana to visualise those traces.
tempo:
Expand Down
31 changes: 31 additions & 0 deletions source/mythical-beasts-frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Dependencies
/node_modules
/.pnp
.pnp.js

# Testing
/coverage

# Production
/build

# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
Thumbs.db
39 changes: 39 additions & 0 deletions source/mythical-beasts-frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Build stage
FROM node:18-alpine AS build

# Accept build arguments
ARG REACT_APP_API_URL
ARG REACT_APP_ALLOY_ENDPOINT

# Set as environment variables for the build process
ENV REACT_APP_API_URL=$REACT_APP_API_URL
ENV REACT_APP_ALLOY_ENDPOINT=$REACT_APP_ALLOY_ENDPOINT

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install all dependencies (including dev dependencies needed for build)
RUN npm ci

# Copy source code
COPY . .

# Build the React app with rsbuild
RUN npm run build

# Production stage
FROM nginx:alpine

# Copy custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf

# Copy built app from build stage
COPY --from=build /app/build /usr/share/nginx/html

# Expose port 80
EXPOSE 80

# Start nginx
CMD ["nginx", "-g", "daemon off;"]
81 changes: 81 additions & 0 deletions source/mythical-beasts-frontend/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log;

# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;

# Server configuration
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;

# Handle React Router (client-side routing)
location / {
try_files $uri $uri/ /index.html;
}

# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

# API proxy to backend - proxy API requests to the mythical-server
location /api/ {
# Remove /api prefix and proxy to backend
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://mythical-server:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# CORS headers for direct API access via nginx
add_header Access-Control-Allow-Origin "http://localhost:3001" always;
add_header Access-Control-Allow-Methods "GET, POST, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials "true" always;

# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin "http://localhost:3001";
add_header Access-Control-Allow-Methods "GET, POST, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
add_header Access-Control-Allow-Credentials "true";
add_header Content-Length 0;
add_header Content-Type text/plain;
return 204;
}
}

# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}
Loading
Loading