Oracle GoldenGate Data Streams to F1 Telemetry with AsyncAPI
Since GoldenGate 23ai was announced, it came with a great functionality called Data Streams. This allows you to subscribe to database changes in real time — with a clean pub/sub model that’s easy to consume from your apps, thanks to the AsyncAPI standard.
GG Data Streams makes it simple for you and your app to “subscribe” to changes in any table you configure. Instead of polling, you get a JSON message via websocket secure as soon as event happens. You could build real-time analytics, monitoring, alerts, dashboards — you name it.
So I decided to try something fun: visualize F1 car telemetry streaming straight from GoldenGate into a real-time dashboard in my browser. Below you could see the targeted end result.
This is I wanted to achieve — and here exactly how I did it. (This article will not show how to create extracts and connect the DB to OGG — there are plenty of articles about that)
GoldenGate configuration
- Open GoldenGate/OCI GoldenGate and navigate to Distribution Service.
- Click on the + button to add a new Data Stream.
3. You will have to provide some basic information, like the Stream Name, Trail name, etc.
4. In my scenario I don’t make any modifications — leaving all values as defaults.
5. Once successful, you will your Data Streams created and would be appear on the left panel under Data Streams category. It will contains information about the stream but also a YAML file — which is very important as based on it we will create our client.
This is it — Congratulations! You have created your first stream.
Next, we leave GG aside and we do app preparation.
APP preparation
Some prerequisites. Make sure you have the following installed before proceeding.
npm install
npm install -g @asyncapi/generatorCheck all with:
node -v
npm -v
git -vNow, clone the Oracle’s WebSocket Client template
git clone https://github.com/oracle-samples/websocket-client-template.gitThis step is pretty important because you cannot generate a client code without a template directory as AsyncAPI Generator requires a YAML file (we will get it from GG) and a template directory. Once cloned, you could see all the client examples in the /output directory.
Get Alexandru Porcescu’s stories in your inbox
Join Medium for free to get updates from this writer.
Next, do a:
cd websocket-client-template
vi F1Data.YAMLCopy the generated YAML file from GG and paste it inside. It should look like this:
Do a :wq! to save it.
Next to generate the client/server code from our YAML file, we will use the asyncapi generate command. To know more about it, I strongly recommend looking at https://www.asyncapi.com/docs/tools/generator/usage.
asyncapi generate fromTemplate F1Data.YAML . -o output -p server=10.2.1.32 -p language=all -p authorization=basicThis step is optional. If you have a web-server already you could skip this part. In my particular case I did not and had to create one. I used NodeJS and used it as a proxy that will create a bridge between GG WebSocket stream and my local WebSocket endpoint.
In the /output directory I created a proxy.js that will do the connectivity for me. Also inside this proxy I hardcoded my credentials. I strongly DO NOT RECOMMEND doing it, or at least create a dedicated user in GG with only read operations.
Create the following js file and RUN it.
proxy.js file
const express = require('express');
const WebSocket = require('ws');
const http = require('http');
const app = express();
const USERNAME = 'ggReadUser';
const PASSWORD = 'myPassIsVeryStrong123';
const REMOTE_WS_URL = 'wss://10.2.1.32/services/distsrvr/v2/stream/F1Data?begin=earliest';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
app.use(express.static('.'));
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
wss.on('connection', function browserWS(localWS) {
console.log('Browser connected to local proxy');
const auth = 'Basic ' + Buffer.from(`${USERNAME}:${PASSWORD}`).toString('base64');
const backendWS = new WebSocket(REMOTE_WS_URL, {
headers: { Authorization: auth }
});
backendWS.on('open', () => {
console.log('Connection to WSS is working');
});
backendWS.on('message', (data) => {
localWS.send(data);
});
backendWS.on('close', () => {
localWS.close();
});
backendWS.on('error', (err) => {
localWS.send('Backend error: ' + err.toString());
});
localWS.on('close', () => {
backendWS.close();
});
});
server.listen(8080, () => {
console.log('Proxy server is running on http://localhost:8080 << Go Test it');
});If Successful you should see: Browser connected to local proxy
The easiest is left for last because you could modify it as you want. You could also make it simpler and nicer with e better design. My idea was to stream data into a simple webpage and also displaying small charts that display average tyre pressure and temperature of the last 10 records grouped by Driver (I had 4 of them).
While being in the /output directory where we have the proxy file, I created an index.html file with the following inside it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Live F1 Telemetry Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h2 { text-align: center; }
.main-layout { display: flex; justify-content: space-between; align-items: flex-start; }
#table-container { width: 65%; }
#chart-container { width: 33%; padding-left: 20px; display: flex; flex-direction: column; }
#chart-container canvas { width: 100% !important; height: 300px !important; margin-bottom: 30px; }
</style>
</head>
<body>
<h2>Live Telemetry Data Stream</h2>
<div class="main-layout">
<div id="table-container">
<table id="telemetry-table" aria-label="Live telemetry data table">
<thead>
<tr>
<th>Driver</th>
<th>Team</th>
<th>Sector</th>
<th>Interval</th>
<th>Tyre Pressure</th>
<th>Tyre Type</th>
<th>Tyre Temp</th>
<th>Braking Temp</th>
<th>Lap</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="status" style="margin-top: 20px; font-weight: bold;"></div>
</div>
<div id="chart-container">
<canvas id="driverPressureChart"></canvas>
<canvas id="driverTemperatureChart"></canvas>
</div>
</div>
<script>
const WS_URL = 'ws://localhost:8080';
const statusDiv = document.getElementById('status');
const tbody = document.getElementById('telemetry-table').querySelector('tbody');
const ctxPressure = document.getElementById('driverPressureChart').getContext('2d');
const ctxTemperature = document.getElementById('driverTemperatureChart').getContext('2d');
const driverPressures = {};
const driverTemperatures = {};
const pressureChart = new Chart(ctxPressure, {
type: 'bar',
data:{
labels: [],
datasets: [{
label: 'Avg Tyre Pressure',
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1,
data: []
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: { display: true, text: 'Tyre Pressure' }
}
}
}
});
const temperatureChart = new Chart(ctxTemperature, {
type: 'bar',
data: {
labels: [],
datasets: [{
label: 'Avg Tyre Temperature',
backgroundColor: 'rgba(255, 99, 132, 0.6)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1,
data: []
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: { display: true, text: 'Tyre Temperature (°C)' }
}
}
}
});
let ws;
try {
ws = new WebSocket(WS_URL);
} catch (err) {
statusDiv.textContent = 'Could not connect: ' + err;
}
ws.onopen = () => {
statusDiv.textContent = 'WebSocket connected to ' + WS_URL;
};
function insertRecordIntoTable(rec) {
const data = rec.after || {};
const row = tbody.insertRow(0);
[
data.DRIVER || '',
data.TEAM || '',
data.SECTOR || '',
data.INTERVAL || '',
data.TYRE_PRESSURE || '',
data.TYRE_TYPE || '',
data.TYRE_TEMPERATURE || '',
data.BRAKING_TEMPERATURE || '',
data.LAP || '',
data.TIMESTAMP || ''
].forEach((val, i) => {
const cell = row.insertCell(i);
cell.textContent = val;
});
// <<<<< ONLY LAST 10 RECORDS
if (data.DRIVER && typeof data.TYRE_PRESSURE === 'number') {
if (!driverPressures[data.DRIVER]) driverPressures[data.DRIVER] = [];
driverPressures[data.DRIVER].push(data.TYRE_PRESSURE);
if (driverPressures[data.DRIVER].length > 10) {
driverPressures[data.DRIVER] = driverPressures[data.DRIVER].slice(-10);
}
}
if (data.DRIVER && typeof data.TYRE_TEMPERATURE === 'number') {
if (!driverTemperatures[data.DRIVER]) driverTemperatures[data.DRIVER] = [];
driverTemperatures[data.DRIVER].push(data.TYRE_TEMPERATURE);
if (driverTemperatures[data.DRIVER].length > 10) {
driverTemperatures[data.DRIVER] = driverTemperatures[data.DRIVER].slice(-10);
}
}
}
function updateCharts() {
const drivers = Object.keys(driverPressures);
pressureChart.data.labels = drivers;
pressureChart.data.datasets[0].data = drivers.map(driver => {
const pressures = driverPressures[driver];
if (!pressures || pressures.length === 0) return 0;
const avgPressure = pressures.reduce((sum, p) => sum + p, 0) / pressures.length;
return Number(avgPressure.toFixed(2));
});
pressureChart.update();
temperatureChart.data.labels = drivers;
temperatureChart.data.datasets[0].data = drivers.map(driver => {
const temps = driverTemperatures[driver];
if (!temps || temps.length === 0) return 0;
const avgTemp = temps.reduce((sum, t) => sum + t, 0) / temps.length;
return Number(avgTemp.toFixed(2));
});
temperatureChart.update();
}
ws.onmessage = event => {
function handleRecords(records) {
if (!Array.isArray(records)) records = [records];
records.forEach(record => insertRecordIntoTable(record));
updateCharts();
}
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.onload = function() {
try {
handleRecords(JSON.parse(reader.result));
} catch {
handleRecords([reader.result]);
}
};
reader.readAsText(event.data);
} else {
try {
handleRecords(JSON.parse(event.data));
} catch {
handleRecords([event.data]);
}
}
};
ws.onerror = err => {
statusDiv.textContent = 'WebSocket error: ' + JSON.stringify(err);
};
ws.onclose = event => {
statusDiv.textContent = 'WebSocket closed. code=' + event.code + ' reason=' + event.reason + ' wasClean=' + event.wasClean;
};
</script>
</body>
</html>In the browser, while the proxy is till running, navigate to: http://localhost:8080/index.html . Just in case you could inspect the page looking at the Console for any eventual errors.
In conclusion wanted to summarize — With OGG Data Streams you get a solution that’s fast, flexible, and open — ready for you to customize for any business case, analytics scenario, or DevOps project. Whether you stream Formula 1 telemetry, financial events, IoT, or supply chain data — you now have the blueprint to get live data flowing straight from GoldenGate to your browser.









