Export map tiles usage to Prometheus

This commit is contained in:
Eugene Burmakin 2025-02-11 20:45:36 +01:00
parent d2d6f95322
commit 1580fb8ade
12 changed files with 152 additions and 12 deletions

View file

@ -16,6 +16,13 @@ To set a custom tile URL, go to the user settings and set the `Maps` section to
- Safe settings for user with default values. - Safe settings for user with default values.
- In the user settings, you can now set a custom tile URL for the map. #429 #715 - In the user settings, you can now set a custom tile URL for the map. #429 #715
- If you have Prometheus exporter enabled, you can now see a `ruby_dawarich_map_tiles` metric in Prometheus, which shows the total number of map tiles loaded. Example:
```
# HELP ruby_dawarich_map_tiles
# TYPE ruby_dawarich_map_tiles gauge
ruby_dawarich_map_tiles 99
```
# 0.24.0 - 2025-02-10 # 0.24.0 - 2025-02-10

View file

@ -1,2 +1,2 @@
prometheus_exporter: bundle exec prometheus_exporter -b ANY prometheus_exporter: bundle exec prometheus_exporter -b ANY
web: bin/rails server -p 3000 -b :: web: bin/rails server -p 3000 -b ::

View file

@ -28,6 +28,7 @@ Donate using crypto: [0x6bAd13667692632f1bF926cA9B421bEe7EaEB8D4](https://ethers
- Explore statistics like the number of countries and cities visited, total distance traveled, and more! - Explore statistics like the number of countries and cities visited, total distance traveled, and more!
📄 **Changelog**: Find the latest updates [here](CHANGELOG.md). 📄 **Changelog**: Find the latest updates [here](CHANGELOG.md).
👩‍💻 **Contribute**: See [CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute to Dawarich. 👩‍💻 **Contribute**: See [CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute to Dawarich.
--- ---

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Api::V1::TileUsagesController < ApiController
def create
TileUsage::Track.new(params[:tile_count].to_i).call
head :ok
end
end

View file

@ -8,9 +8,7 @@ import { createMarkersArray } from "../maps/markers";
import { import {
createPolylinesLayer, createPolylinesLayer,
updatePolylinesOpacity, updatePolylinesOpacity,
updatePolylinesColors, updatePolylinesColors
calculateSpeed,
getSpeedColor
} from "../maps/polylines"; } from "../maps/polylines";
import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas"; import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas";
@ -32,9 +30,13 @@ import { countryCodesMap } from "../maps/country_codes";
import "leaflet-draw"; import "leaflet-draw";
import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war"; import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war";
import { TileMonitor } from "../maps/tile_monitor";
export default class extends Controller { export default class extends Controller {
static targets = ["container"]; static targets = ["container"];
static values = {
monitoringEnabled: Boolean
}
settingsButtonAdded = false; settingsButtonAdded = false;
layerControl = null; layerControl = null;
@ -245,6 +247,19 @@ export default class extends Controller {
if (this.liveMapEnabled) { if (this.liveMapEnabled) {
this.setupSubscription(); this.setupSubscription();
} }
// Initialize tile monitor
this.tileMonitor = new TileMonitor(this.monitoringEnabledValue, this.apiKey);
// Add tile load event handlers to each base layer
Object.entries(this.baseMaps()).forEach(([name, layer]) => {
layer.on('tileload', () => {
this.tileMonitor.recordTileLoad(name);
});
});
// Start monitoring
this.tileMonitor.startMonitoring();
} }
disconnect() { disconnect() {
@ -260,6 +275,11 @@ export default class extends Controller {
if (this.map) { if (this.map) {
this.map.remove(); this.map.remove();
} }
// Stop tile monitoring
if (this.tileMonitor) {
this.tileMonitor.stopMonitoring();
}
} }
setupSubscription() { setupSubscription() {

View file

@ -0,0 +1,67 @@
export class TileMonitor {
constructor(monitoringEnabled, apiKey) {
this.monitoringEnabled = monitoringEnabled;
this.apiKey = apiKey;
this.tileQueue = 0;
this.tileUpdateInterval = null;
}
startMonitoring() {
// Only start the interval if monitoring is enabled
if (!this.monitoringEnabled) return;
// Clear any existing interval
if (this.tileUpdateInterval) {
clearInterval(this.tileUpdateInterval);
}
// Set up a regular interval to send stats
this.tileUpdateInterval = setInterval(() => {
this.sendTileUsage();
}, 5000); // Exactly every 5 seconds
}
stopMonitoring() {
if (this.tileUpdateInterval) {
clearInterval(this.tileUpdateInterval);
this.sendTileUsage(); // Send any remaining stats
}
}
recordTileLoad() {
if (!this.monitoringEnabled) return;
this.tileQueue += 1;
}
sendTileUsage() {
// Don't send if monitoring is disabled or queue is empty
if (!this.monitoringEnabled || this.tileQueue === 0) return;
const currentCount = this.tileQueue;
console.log('Sending tile usage batch:', currentCount);
fetch('/api/v1/tile_usages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
tile_count: currentCount
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Only subtract sent count if it hasn't changed
if (this.tileQueue === currentCount) {
this.tileQueue = 0;
} else {
this.tileQueue -= currentCount;
}
console.log('Tile usage batch sent successfully');
})
.catch(error => console.error('Error recording tile usage:', error));
}
}

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
class TileUsage::Track
def initialize(count = 1)
@count = count
end
def call
metric_data = {
type: 'counter',
name: 'dawarich_map_tiles',
value: @count
}
PrometheusExporter::Client.default.send_json(metric_data)
rescue StandardError => e
Rails.logger.error("Failed to send tile usage metric: #{e.message}")
end
end

View file

@ -53,6 +53,7 @@
data-coordinates="<%= @coordinates %>" data-coordinates="<%= @coordinates %>"
data-distance="<%= @distance %>" data-distance="<%= @distance %>"
data-points_number="<%= @points_number %>" data-points_number="<%= @points_number %>"
data-maps-monitoring-enabled-value="<%= DawarichSettings.prometheus_exporter_enabled? %>"
data-timezone="<%= Rails.configuration.time_zone %>"> data-timezone="<%= Rails.configuration.time_zone %>">
<div data-maps-target="container" class="h-[25rem] rounded-lg w-full min-h-screen"> <div data-maps-target="container" class="h-[25rem] rounded-lg w-full min-h-screen">
<div id="fog" class="fog"></div> <div id="fog" class="fog"></div>

View file

@ -3,10 +3,25 @@
<div class="min-h-content w-full my-5"> <div class="min-h-content w-full my-5">
<%= render 'settings/navigation' %> <%= render 'settings/navigation' %>
<div class="flex justify-between items-center mt-5"> <div class="flex justify-between items-center my-5">
<h1 class="font-bold text-4xl">Maps settings</h1> <h1 class="font-bold text-4xl">Maps settings</h1>
</div> </div>
<div role="alert" class="alert alert-info">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>Please remember, that using a custom tile URL may result in extra costs. Check your map tile provider's terms of service for more information.</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-5" data-controller="map-preview"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-5" data-controller="map-preview">
<%= form_for :maps, <%= form_for :maps,
url: settings_maps_path, url: settings_maps_path,

View file

@ -18,12 +18,11 @@ class DawarichSettings
@geoapify_enabled ||= GEOAPIFY_API_KEY.present? @geoapify_enabled ||= GEOAPIFY_API_KEY.present?
end end
def meters_between_tracks def prometheus_exporter_enabled?
@meters_between_tracks ||= 300 @prometheus_exporter_enabled ||=
end ENV['PROMETHEUS_EXPORTER_ENABLED'].to_s == 'true' &&
ENV['PROMETHEUS_EXPORTER_HOST'].present? &&
def minutes_between_tracks ENV['PROMETHEUS_EXPORTER_PORT'].present?
@minutes_between_tracks ||= 20
end end
end end
end end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
if !Rails.env.test? && ENV['PROMETHEUS_EXPORTER_ENABLED'].to_s == 'true' if !Rails.env.test? && DawarichSettings.prometheus_exporter_enabled?
require 'prometheus_exporter/middleware' require 'prometheus_exporter/middleware'
require 'prometheus_exporter/instrumentation' require 'prometheus_exporter/instrumentation'

View file

@ -96,6 +96,8 @@ Rails.application.routes.draw do
get 'thumbnail', constraints: { id: %r{[^/]+} } get 'thumbnail', constraints: { id: %r{[^/]+} }
end end
end end
resources :tile_usages, only: [:create]
end end
end end
end end