Initial Commit
This commit is contained in:
commit
97c1eca378
8 changed files with 1278 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
13
.woodpacker.yml
Normal file
13
.woodpacker.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
pipeline:
|
||||||
|
build:
|
||||||
|
image: rust:1-alpine
|
||||||
|
commands:
|
||||||
|
- cargo build --release
|
||||||
|
containerize:
|
||||||
|
image: docker:23-dind
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- apk add git
|
||||||
|
- docker buildx build -t git.boerlage.me/daan/osrs-playercount-exporter:latest .
|
||||||
|
- docker push git.boerlage.me/daan/osrs-playercount-exporter:latest
|
1078
Cargo.lock
generated
Normal file
1078
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "osrs-playercount"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
axum = "0.6.12"
|
||||||
|
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls-webpki-roots"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
4
Dockerfile
Normal file
4
Dockerfile
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY ${CI_WORKSPACE}/target/release/osrs-playercount /bootstrap
|
||||||
|
CMD ["/bootstrap"]
|
13
readme.md
Normal file
13
readme.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[![status-badge](https://woodpecker.boerlage.me/api/badges/daan/osrs-playercount-exporter/status.svg)](https://woodpecker.boerlage.me/daan/osrs-playercount-exporter)
|
||||||
|
|
||||||
|
# OSRS Player Count Exporter
|
||||||
|
|
||||||
|
Exposes a prometheus `/metrics` endpoint with oldschool runescape word player count data.
|
||||||
|
|
||||||
|
## Runelite Endpoint
|
||||||
|
|
||||||
|
This leaches off of the following runlite endpoint:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://api.runelite.net/runelite-1.9.14-SNAPSHOT/worlds.js
|
||||||
|
```
|
125
src/collector/mod.rs
Normal file
125
src/collector/mod.rs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::str;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct Worlds {
|
||||||
|
pub worlds: Vec<World>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum WorldLocation {
|
||||||
|
Germany,
|
||||||
|
USA,
|
||||||
|
UnitedKingdom,
|
||||||
|
Australia,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for WorldLocation {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let field = i16::deserialize(deserializer)?;
|
||||||
|
let world = match field {
|
||||||
|
0 => Self::USA,
|
||||||
|
1 => Self::UnitedKingdom,
|
||||||
|
3 => Self::Australia,
|
||||||
|
7 => Self::Germany,
|
||||||
|
_ => Self::Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(world)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for WorldLocation {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
WorldLocation::Germany => write!(f, "Germany"),
|
||||||
|
WorldLocation::USA => write!(f, "USA"),
|
||||||
|
WorldLocation::UnitedKingdom => write!(f, "UK"),
|
||||||
|
WorldLocation::Australia => write!(f, "Australia"),
|
||||||
|
WorldLocation::Unknown => write!(f, "Unknown"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum WorldType {
|
||||||
|
Members,
|
||||||
|
PVP,
|
||||||
|
Bounty,
|
||||||
|
PVPArena,
|
||||||
|
SkillTotal,
|
||||||
|
QuestSpeedrunning,
|
||||||
|
HighRisk,
|
||||||
|
LastManStanding,
|
||||||
|
NoSaveMode,
|
||||||
|
Tournament,
|
||||||
|
FreshStartWorld,
|
||||||
|
Deadman,
|
||||||
|
Seasonal,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for WorldType {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let field = String::deserialize(deserializer)?;
|
||||||
|
let res = match field.as_str() {
|
||||||
|
"MEMBERS" => Self::Members,
|
||||||
|
"PVP" => Self::PVP,
|
||||||
|
"BOUNTY" => Self::Bounty,
|
||||||
|
"PVP_ARENA" => Self::PVPArena,
|
||||||
|
"SKILL_TOTAL" => Self::SkillTotal,
|
||||||
|
"QUEST_SPEEDRUNNING" => Self::QuestSpeedrunning,
|
||||||
|
"HIGH_RISK" => Self::HighRisk,
|
||||||
|
"LAST_MAN_STANDING" => Self::LastManStanding,
|
||||||
|
"NOSAVE_MODE" => Self::NoSaveMode,
|
||||||
|
"TOURNAMENT" => Self::Tournament,
|
||||||
|
"FRESH_START_WORLD" => Self::FreshStartWorld,
|
||||||
|
"DEADMAN" => Self::Deadman,
|
||||||
|
"SEASONAL" => Self::Seasonal,
|
||||||
|
_ => Self::Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct World {
|
||||||
|
pub id: i16,
|
||||||
|
pub address: String,
|
||||||
|
pub activity: String,
|
||||||
|
pub location: WorldLocation,
|
||||||
|
pub players: i16,
|
||||||
|
pub types: Vec<WorldType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PromMetric {
|
||||||
|
fn to_metric_string(self: &Self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PromMetric for World {
|
||||||
|
fn to_metric_string(&self) -> String {
|
||||||
|
//"player_count{id=\"301\",location=\"Germany\,"isMembers=\"true\"} 123"
|
||||||
|
format!(
|
||||||
|
"player_count{{id=\"{}\",location=\"{}\",isMembers=\"{:?}\"}} {}",
|
||||||
|
self.id,
|
||||||
|
self.location,
|
||||||
|
self.is_members(),
|
||||||
|
self.players
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl World {
|
||||||
|
pub fn is_members(&self) -> bool {
|
||||||
|
self.types.contains(&WorldType::Members)
|
||||||
|
}
|
||||||
|
}
|
33
src/main.rs
Normal file
33
src/main.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
mod collector;
|
||||||
|
|
||||||
|
use axum::routing::get;
|
||||||
|
use axum::Router;
|
||||||
|
use collector::{PromMetric, Worlds};
|
||||||
|
|
||||||
|
async fn metrics() -> String {
|
||||||
|
let resp = reqwest::get("https://api.runelite.net/runelite-1.9.13/worlds.js")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.json::<Worlds>()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let metric_strings: Vec<String> = resp
|
||||||
|
.worlds
|
||||||
|
.into_iter()
|
||||||
|
.map(|w| w.to_metric_string())
|
||||||
|
.collect();
|
||||||
|
metric_strings.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let app = Router::new().route("/metrics", get(metrics));
|
||||||
|
|
||||||
|
println!("Starting...");
|
||||||
|
|
||||||
|
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
Loading…
Reference in a new issue