From 466f00dd0ee7ffdc10aa5b7b361b213edc6c8355 Mon Sep 17 00:00:00 2001 From: Gabriel Simmer Date: Fri, 10 Nov 2023 12:19:31 +0000 Subject: [PATCH] Fix linting, build-test-publish workflow, Docker image --- .forgejo/workflows/build-test.yml | 110 +++++++++++++ Cargo.lock | 7 - Cargo.toml | 2 +- flake.nix | 263 ++++++++++++++++-------------- src/main.rs | 101 ++++++------ 5 files changed, 302 insertions(+), 181 deletions(-) create mode 100644 .forgejo/workflows/build-test.yml diff --git a/.forgejo/workflows/build-test.yml b/.forgejo/workflows/build-test.yml new file mode 100644 index 0000000..75bc8eb --- /dev/null +++ b/.forgejo/workflows/build-test.yml @@ -0,0 +1,110 @@ +name: Build Docker Image +on: + push: + branches: + - master + +jobs: + nix-flake-check: + runs-on: debian-latest + steps: + - name: Install prerequisites + run: apt update && apt install -y sudo zstd + + - name: "Cache Nix store" + uses: actions/cache@v3.0.8 + id: nix-cache + with: + path: /nix + key: "vr-event-tracker-cache-v1" + + - name: Install Nix + uses: https://github.com/cachix/install-nix-action@v22 + with: + extra_nix_config: "experimental-features = nix-command flakes" + nix_path: nixpkgs=channel:nixos-23.05 + - name: Remove access_tokens + run: sed -i '/^access-tokens/d' /etc/nix/nix.conf + + - name: Check out repository + uses: actions/checkout@v3.5.3 + with: + ref: master + - name: Check codebase + run: nix flake check -L + + docker-build: + needs: nix-flake-check + runs-on: debian-latest + steps: + - name: Install prerequisites + run: apt update && apt install -y sudo zstd + + + - name: "Cache Nix store" + uses: actions/cache@v3.0.8 + id: nix-cache + with: + path: /nix + key: "vr-event-tracker-cache-v1" + + - name: Install Nix + uses: https://github.com/cachix/install-nix-action@v22 + with: + extra_nix_config: "experimental-features = nix-command flakes" + nix_path: nixpkgs=channel:nixos-23.05 + - name: Remove access_tokens + run: sed -i '/^access-tokens/d' /etc/nix/nix.conf + + - name: Check out repository + uses: actions/checkout@v3.5.3 + with: + ref: master + - name: Build image + run: nix build .#docker + - name: Push image with Skopeo + run: | + nix-env -i skopeo -f '' + wget https://raw.githubusercontent.com/containers/skopeo/main/default-policy.json && mkdir /etc/containers && mv default-policy.json /etc/containers/policy.json + skopeo login --username arch --password $REGISTRY_TOKEN git.gmem.ca + skopeo copy docker-archive:result docker://git.gmem.ca/arch/vrchat-prometheus-adapter:latest + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + + arm-docker-build: + needs: nix-flake-check + runs-on: debian-latest-arm + steps: + - name: Install prerequisites + run: apt update && apt install -y sudo zstd + + + - name: "Cache Nix store" + uses: actions/cache@v3.0.8 + id: nix-cache + with: + path: /nix + key: "vr-event-tracker-cache-arm-v1" + + - name: Install Nix + uses: https://github.com/cachix/install-nix-action@v22 + with: + extra_nix_config: "experimental-features = nix-command flakes" + nix_path: nixpkgs=channel:nixos-23.05 + - name: Remove access_tokens + run: sed -i '/^access-tokens/d' /etc/nix/nix.conf + + - name: Check out repository + uses: actions/checkout@v3.5.3 + with: + ref: master + - name: Build image + run: nix build .#docker + - name: Push image with Skopeo + run: | + nix-env -i skopeo -f '' + wget https://raw.githubusercontent.com/containers/skopeo/main/default-policy.json && mkdir /etc/containers && mv default-policy.json /etc/containers/policy.json + skopeo login --username arch --password $REGISTRY_TOKEN git.gmem.ca + skopeo copy docker-archive:result docker://git.gmem.ca/arch/vrchat-prometheus-adapter:arm + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 5a15261..fc1577c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -670,16 +670,9 @@ dependencies = [ "lazy_static", "memchr", "parking_lot", - "protobuf", "thiserror", ] -[[package]] -name = "protobuf" -version = "2.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" - [[package]] name = "quote" version = "1.0.33" diff --git a/Cargo.toml b/Cargo.toml index f175011..e7e85c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] axum = "0.6.20" -prometheus = "0.13.3" +prometheus = { version = "0.13.3", default-features = false } tokio = { version = "1.29.1", features = [ "full" ] } serde = { version = "1.0.189", features = [ "derive" ] } toml = "0.8.2" diff --git a/flake.nix b/flake.nix index b2608f3..8b72ed8 100644 --- a/flake.nix +++ b/flake.nix @@ -23,130 +23,151 @@ }; }; - outputs = { self, nixpkgs, crane, fenix, flake-utils, advisory-db, ... }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { - inherit system; + outputs = { self, nixpkgs, crane, fenix, flake-utils, advisory-db, ... }: { + nixosModules.default = ({ pkgs, ... }: { + imports = [ ./module.nix ]; + # defined overlays injected by the nixflake + nixpkgs.overlays = [ + (_self: _super: { + vrchat-prometheus-adapter = self.packages.${pkgs.system}.vrchat-prometheus-adapter; + }) + ]; + }); + } // + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + }; + + inherit (pkgs) lib; + + craneLib = crane.lib.${system}; + src = lib.cleanSourceWith { + src = craneLib.path ./.; # The original, unfiltered source + filter = path: type: + let + protoFilter = path: _type: builtins.match ".*proto$" path != null; + protoOrCargo = path: type: + (protoFilter path type) || (craneLib.filterCargoSources path type); + in + protoOrCargo path type; + }; + # Common arguments can be set here to avoid repeating them later + commonArgs = { + inherit src; + buildInputs = [ + pkgs.sqlite + pkgs.pkg-config + pkgs.openssl + pkgs.protobuf + ] ++ lib.optionals pkgs.stdenv.isDarwin [ + # Additional darwin specific inputs can be set here + pkgs.libiconv + ]; + }; + + craneLibLLvmTools = craneLib.overrideToolchain + (fenix.packages.${system}.complete.withComponents [ + "cargo" + "llvm-tools" + "rustc" + ]); + + # Build *just* the cargo dependencies, so we can reuse + # all of that work (e.g. via cachix) when running in CI + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + + # Build the actual crate itself, reusing the dependency + # artifacts from above. + + my-crate = craneLib.buildPackage (commonArgs // { + inherit cargoArtifacts; + }); + + dockerImage = pkgs.dockerTools.buildImage { + name = "vrchat-prometheus-exporter"; + config = { + Cmd = [ "${my-crate}/bin/vr-event-tracker" ]; + ExposedPorts = { + "6534/tcp" = {}; + }; + }; + }; + + in + { + checks = { + # Build the crate as part of `nix flake check` for convenience + inherit my-crate; + + # Run clippy (and deny all warnings) on the crate source, + # again, resuing the dependency artifacts from above. + # + # Note that this is done as a separate derivation so that + # we can block the CI if there are issues here, but not + # prevent downstream consumers from building our crate by itself. + my-crate-clippy = craneLib.cargoClippy (commonArgs // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets -- --deny warnings"; + }); + + my-crate-doc = craneLib.cargoDoc (commonArgs // { + inherit cargoArtifacts; + }); + + # Check formatting + my-crate-fmt = craneLib.cargoFmt { + inherit src; + }; + + # Audit dependencies + my-crate-audit = craneLib.cargoAudit { + inherit src advisory-db; + }; + + # Run tests with cargo-nextest + # Consider setting `doCheck = false` on `my-crate` if you do not want + # the tests to run twice + my-crate-nextest = craneLib.cargoNextest (commonArgs // { + inherit cargoArtifacts; + partitions = 1; + partitionType = "count"; + }); + } // lib.optionalAttrs (system == "x86_64-linux") { + # NB: cargo-tarpaulin only supports x86_64 systems + # Check code coverage (note: this will not upload coverage anywhere) + my-crate-coverage = craneLib.cargoTarpaulin (commonArgs // { + inherit cargoArtifacts; + }); }; - inherit (pkgs) lib; - - craneLib = crane.lib.${system}; - src = lib.cleanSourceWith { - src = craneLib.path ./.; # The original, unfiltered source - filter = path: type: - let - protoFilter = path: _type: builtins.match ".*proto$" path != null; - protoOrCargo = path: type: - (protoFilter path type) || (craneLib.filterCargoSources path type); - in - protoOrCargo path type; + packages = { + default = my-crate; + docker = dockerImage; + my-crate-llvm-coverage = craneLibLLvmTools.cargoLlvmCov (commonArgs // { + inherit cargoArtifacts; + }); }; - # Common arguments can be set here to avoid repeating them later - commonArgs = { - inherit src; - buildInputs = [ - pkgs.sqlite - pkgs.pkg-config - pkgs.openssl - pkgs.protobuf - ] ++ lib.optionals pkgs.stdenv.isDarwin [ - # Additional darwin specific inputs can be set here - pkgs.libiconv + + apps.default = flake-utils.lib.mkApp { + drv = my-crate; + }; + + devShells.default = pkgs.mkShell { + inputsFrom = builtins.attrValues self.checks.${system}; + + # Additional dev-shell environment variables can be set directly + # MY_CUSTOM_DEVELOPMENT_VAR = "something else"; + + # Extra inputs can be added here + nativeBuildInputs = with pkgs; [ + cargo + rustc + rust-analyzer + cargo-flamegraph + cargo-bloat ]; }; - - craneLibLLvmTools = craneLib.overrideToolchain - (fenix.packages.${system}.complete.withComponents [ - "cargo" - "llvm-tools" - "rustc" - ]); - - # Build *just* the cargo dependencies, so we can reuse - # all of that work (e.g. via cachix) when running in CI - cargoArtifacts = craneLib.buildDepsOnly commonArgs; - - # Build the actual crate itself, reusing the dependency - # artifacts from above. - - my-crate = craneLib.buildPackage (commonArgs // { - inherit cargoArtifacts; - }); - in - { - checks = { - # Build the crate as part of `nix flake check` for convenience - inherit my-crate; - - # Run clippy (and deny all warnings) on the crate source, - # again, resuing the dependency artifacts from above. - # - # Note that this is done as a separate derivation so that - # we can block the CI if there are issues here, but not - # prevent downstream consumers from building our crate by itself. - my-crate-clippy = craneLib.cargoClippy (commonArgs // { - inherit cargoArtifacts; - cargoClippyExtraArgs = "--all-targets -- --deny warnings"; - }); - - my-crate-doc = craneLib.cargoDoc (commonArgs // { - inherit cargoArtifacts; - }); - - # Check formatting - my-crate-fmt = craneLib.cargoFmt { - inherit src; - }; - - # Audit dependencies - my-crate-audit = craneLib.cargoAudit { - inherit src advisory-db; - }; - - # Run tests with cargo-nextest - # Consider setting `doCheck = false` on `my-crate` if you do not want - # the tests to run twice - my-crate-nextest = craneLib.cargoNextest (commonArgs // { - inherit cargoArtifacts; - partitions = 1; - partitionType = "count"; - }); - } // lib.optionalAttrs (system == "x86_64-linux") { - # NB: cargo-tarpaulin only supports x86_64 systems - # Check code coverage (note: this will not upload coverage anywhere) - my-crate-coverage = craneLib.cargoTarpaulin (commonArgs // { - inherit cargoArtifacts; - }); - }; - - packages = { - default = my-crate; - my-crate-llvm-coverage = craneLibLLvmTools.cargoLlvmCov (commonArgs // { - inherit cargoArtifacts; - }); - }; - - apps.default = flake-utils.lib.mkApp { - drv = my-crate; - }; - - devShells.default = pkgs.mkShell { - inputsFrom = builtins.attrValues self.checks.${system}; - - # Additional dev-shell environment variables can be set directly - # MY_CUSTOM_DEVELOPMENT_VAR = "something else"; - - # Extra inputs can be added here - nativeBuildInputs = with pkgs; [ - cargo - rustc - rust-analyzer - sqlite - sqlx-cli - cargo-flamegraph - ]; - }; - }); + }); } diff --git a/src/main.rs b/src/main.rs index fcee6f6..216b278 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,6 @@ static WORLD_FAVORITES: OnceLock = OnceLock::new(); enum WsError { Reqwest(reqwest::Error), Url(url::ParseError), - Custom(String), } impl From for WsError { @@ -49,7 +48,6 @@ impl fmt::Display for WsError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { WsError::Reqwest(e) => write!(f, "Reqwest error: {}", e), - WsError::Custom(e) => write!(f, "Error: {}", e), WsError::Url(_) => todo!(), } } @@ -107,10 +105,7 @@ struct VrcGroupInstance { #[derive(Clone, Debug, Deserialize)] struct VrcWorldData { favorites: u64, - version: f64, visits: u64, - popularity: f64, - heat: f64, #[serde(rename = "publicOccupants")] public_occupants: f64, #[serde(rename = "privateOccupants")] @@ -227,42 +222,12 @@ async fn group_metrics( name: String, group: VrcGroup, ) -> Result<(), WsError> { - let instance_list_url = format!( - "https://api.vrchat.cloud/api/1/groups/{}/instances", - group.id - ); - let url = Url::parse(&instance_list_url).unwrap(); - let req = client - .get(url) - .header( - USER_AGENT, - "vr-event-tracker(git.gmem.ca/arch/vr-event-tracker)", - ) - .header("Cookie", auth_cookie) - .send() - .await?; - - let data: Vec = req.json().await?; - let instances: Vec = data - .into_iter() - .map(|f| { - let spl: Vec<&str> = f.location.split(":").collect(); - VrcInstance { - instance: Some(spl[0].to_owned()), - world: Some(spl[1].to_owned()), - location: Some(f.location), - name: None, - } - }) - .collect(); - - for instance in instances { - let api_url = format!( - "https://api.vrchat.cloud/api/1/instances/{}", - &instance.location.clone().unwrap() + if !group.id.is_empty() { + let instance_list_url = format!( + "https://api.vrchat.cloud/api/1/groups/{}/instances", + group.id ); - let url = Url::parse(&api_url).unwrap(); - + let url = Url::parse(&instance_list_url).unwrap(); let req = client .get(url) .header( @@ -273,18 +238,50 @@ async fn group_metrics( .send() .await?; - let data: VrcInstanceData = req.json().await?; - let instance_name = instance.name.unwrap_or(instance.location.unwrap()); - PLAYER_COUNT - .get() - .unwrap() - .with_label_values(&[ - &instance.world.unwrap(), - &instance.instance.unwrap(), - &instance_name, - &name, - ]) - .set(data.user_count.unwrap_or(0 as f64)); + let data: Vec = req.json().await?; + let instances: Vec = data + .into_iter() + .map(|f| { + let spl: Vec<&str> = f.location.split(':').collect(); + VrcInstance { + instance: Some(spl[0].to_owned()), + world: Some(spl[1].to_owned()), + location: Some(f.location), + name: None, + } + }) + .collect(); + + for instance in instances { + let api_url = format!( + "https://api.vrchat.cloud/api/1/instances/{}", + &instance.location.clone().unwrap() + ); + let url = Url::parse(&api_url).unwrap(); + + let req = client + .get(url) + .header( + USER_AGENT, + "vr-event-tracker(git.gmem.ca/arch/vr-event-tracker)", + ) + .header("Cookie", auth_cookie) + .send() + .await?; + + let data: VrcInstanceData = req.json().await?; + let instance_name = instance.name.unwrap_or(instance.location.unwrap()); + PLAYER_COUNT + .get() + .unwrap() + .with_label_values(&[ + &instance.world.unwrap(), + &instance.instance.unwrap(), + &instance_name, + &name, + ]) + .set(data.user_count.unwrap_or(0 as f64)); + } } if group.vrcdn.is_some() {