Skip to content

Commit a4cf625

Browse files
authored
feat(stats): reset charts (#1226)
- add authorization w/ api keys - on-demand chart update - chart reupdate logic - api-key protected endpoint for requesting the chart reupdates - readability / code structure improvements - tests performance improvements - fix last_accurate_point for counters
1 parent d02877c commit a4cf625

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1976
-618
lines changed

.github/workflows/stats.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
if: success() || failure()
6767

6868
- name: DB tests
69-
run: RUST_BACKTRACE=1 RUST_LOG=info cargo test --locked --workspace -- --nocapture --ignored
69+
run: RUST_BACKTRACE=1 cargo test --locked --workspace -- --nocapture --ignored
7070
if: success() || failure()
7171
env:
7272
DATABASE_URL: postgres://postgres:admin@localhost:5432/

stats/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

stats/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ RUN cargo chef cook --release --recipe-path recipe.json
1010

1111
FROM chef AS build
1212

13+
# Include proto common definitions (`proto` must be passed as build context)
14+
COPY --from=proto . /proto
1315
COPY . .
1416
COPY --from=cache /app/target target
1517
COPY --from=cache $CARGO_HOME $CARGO_HOME

stats/justfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export DATABASE_URL := "postgres://" + db-user + ":" + db-password + "@" + db-ho
1111
docker-name := env_var_or_default('DOCKER_NAME', "stats-postgres")
1212
test-db-port := env_var_or_default('TEST_DB_PORT', "9433")
1313

14+
docker-build *args:
15+
docker build --build-context proto=../proto . {{args}}
16+
1417
start-postgres:
1518
# we run it in --rm mode, so all data will be deleted after stopping
1619
docker run -p {{db-port}}:5432 --name {{docker-name}} -e POSTGRES_PASSWORD={{db-password}} -e POSTGRES_USER={{db-user}} --rm -d postgres -N 500

stats/stats-proto/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
4141
]));
4242
compile(
4343
&["proto/stats.proto", "proto/health.proto"],
44-
&["proto"],
44+
&["proto", "../../proto"],
4545
gens,
4646
)?;
4747
Ok(())

stats/stats-proto/proto/api_config_http.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ http:
1515
get: /api/v1/pages/transactions
1616
- selector: blockscout.stats.v1.StatsService.GetContractsPageStats
1717
get: /api/v1/pages/contracts
18+
- selector: blockscout.stats.v1.StatsService.BatchUpdateCharts
19+
post: /api/v1/charts/batch-update
20+
body: "*"
1821

1922
- selector: blockscout.stats.v1.Health.Check
2023
get: /health

stats/stats-proto/proto/stats.proto

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,53 @@ syntax = "proto3";
22

33
package blockscout.stats.v1;
44

5+
import "protoc-gen-openapiv2/options/annotations.proto";
6+
7+
58
option go_package = "github.com/blockscout/blockscout-rs/stats";
69

10+
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
11+
info: {
12+
title: "Stats service";
13+
contact: {
14+
name: "Blockscout";
15+
url: "https://blockscout.com";
16+
17+
};
18+
};
19+
schemes: [HTTPS],
20+
external_docs: {
21+
url: "https://github.com/blockscout/blockscout-rs";
22+
description: "More about blockscout microservices";
23+
}
24+
security_definitions: {
25+
security: {
26+
key: "ApiKeyAuth";
27+
value: {
28+
type: TYPE_API_KEY;
29+
in: IN_HEADER;
30+
name: "x-api-key";
31+
}
32+
}
33+
}
34+
};
35+
36+
737
service StatsService {
838
rpc GetCounters(GetCountersRequest) returns (Counters);
939
rpc GetLineCharts(GetLineChartsRequest) returns (LineCharts);
1040
rpc GetLineChart(GetLineChartRequest) returns (LineChart);
1141
rpc GetMainPageStats(GetMainPageStatsRequest) returns (MainPageStats);
1242
rpc GetTransactionsPageStats(GetTransactionsPageStatsRequest) returns (TransactionsPageStats);
1343
rpc GetContractsPageStats(GetContractsPageStatsRequest) returns (ContractsPageStats);
44+
rpc BatchUpdateCharts(BatchUpdateChartsRequest) returns (BatchUpdateChartsResult) {
45+
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
46+
security: {
47+
security_requirement: {key: "ApiKeyAuth"}
48+
}
49+
tags: "Instances"
50+
};
51+
};
1452
}
1553

1654
message GetCountersRequest {}
@@ -109,3 +147,22 @@ message ContractsPageStats {
109147
optional Counter total_verified_contracts = 3;
110148
optional Counter new_verified_contracts_24h = 4;
111149
}
150+
151+
message BatchUpdateChartsRequest {
152+
repeated string chart_names = 1;
153+
// Default is today
154+
optional string from = 2;
155+
optional bool update_later = 3;
156+
}
157+
158+
message BatchUpdateChartRejection {
159+
string name = 1;
160+
string reason = 2;
161+
}
162+
163+
message BatchUpdateChartsResult {
164+
uint32 total = 1;
165+
uint32 total_rejected = 2;
166+
repeated string accepted = 3;
167+
repeated BatchUpdateChartRejection rejected = 4;
168+
}

stats/stats-proto/swagger/stats.swagger.yaml

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,43 @@
11
swagger: "2.0"
22
info:
3-
title: stats.proto
3+
title: Stats service
44
version: version not set
5+
contact:
6+
name: Blockscout
7+
url: https://blockscout.com
8+
59
tags:
610
- name: StatsService
711
- name: Health
12+
schemes:
13+
- https
814
consumes:
915
- application/json
1016
produces:
1117
- application/json
1218
paths:
19+
/api/v1/charts/batch-update:
20+
post:
21+
operationId: StatsService_BatchUpdateCharts
22+
responses:
23+
"200":
24+
description: A successful response.
25+
schema:
26+
$ref: '#/definitions/v1BatchUpdateChartsResult'
27+
default:
28+
description: An unexpected error response.
29+
schema:
30+
$ref: '#/definitions/rpcStatus'
31+
parameters:
32+
- name: body
33+
in: body
34+
required: true
35+
schema:
36+
$ref: '#/definitions/v1BatchUpdateChartsRequest'
37+
tags:
38+
- Instances
39+
security:
40+
- ApiKeyAuth: []
1341
/api/v1/counters:
1442
get:
1543
operationId: StatsService_GetCounters
@@ -171,6 +199,43 @@ definitions:
171199
items:
172200
type: object
173201
$ref: '#/definitions/protobufAny'
202+
v1BatchUpdateChartRejection:
203+
type: object
204+
properties:
205+
name:
206+
type: string
207+
reason:
208+
type: string
209+
v1BatchUpdateChartsRequest:
210+
type: object
211+
properties:
212+
chart_names:
213+
type: array
214+
items:
215+
type: string
216+
from:
217+
type: string
218+
title: Default is today
219+
update_later:
220+
type: boolean
221+
v1BatchUpdateChartsResult:
222+
type: object
223+
properties:
224+
total:
225+
type: integer
226+
format: int64
227+
total_rejected:
228+
type: integer
229+
format: int64
230+
accepted:
231+
type: array
232+
items:
233+
type: string
234+
rejected:
235+
type: array
236+
items:
237+
type: object
238+
$ref: '#/definitions/v1BatchUpdateChartRejection'
174239
v1ContractsPageStats:
175240
type: object
176241
properties:
@@ -308,3 +373,11 @@ definitions:
308373
$ref: '#/definitions/v1Counter'
309374
operational_transactions_24h:
310375
$ref: '#/definitions/v1Counter'
376+
securityDefinitions:
377+
ApiKeyAuth:
378+
type: apiKey
379+
name: x-api-key
380+
in: header
381+
externalDocs:
382+
description: More about blockscout microservices
383+
url: https://github.com/blockscout/blockscout-rs

stats/stats-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ config = "0.13"
1818
tracing = "0.1"
1919
futures = "0.3"
2020
anyhow = "1.0"
21+
thiserror = "1.0"
2122
chrono = "0.4"
2223
sea-orm = { workspace = true, features = [
2324
"sqlx-postgres",

stats/stats-server/src/auth.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use std::collections::HashMap;
2+
3+
use serde::{Deserialize, Serialize};
4+
use tonic::Status;
5+
6+
pub struct AuthorizationProvider {
7+
keys: HashMap<String, ApiKey>,
8+
}
9+
10+
pub const API_KEY_NAME: &str = "x-api-key";
11+
12+
impl AuthorizationProvider {
13+
pub fn new(keys: HashMap<String, ApiKey>) -> Self {
14+
Self { keys }
15+
}
16+
17+
pub fn is_request_authorized<T>(&self, request: &tonic::Request<T>) -> bool {
18+
let Some(key) = request.metadata().get(API_KEY_NAME) else {
19+
return false;
20+
};
21+
let Ok(api_key) = key
22+
.to_str()
23+
.inspect_err(|e| tracing::warn!("could not read http header as ascii: {}", e))
24+
else {
25+
return false;
26+
};
27+
self.is_key_authorized(api_key)
28+
}
29+
30+
pub fn is_key_authorized(&self, api_key: &str) -> bool {
31+
self.keys.values().any(|key| key.key.eq(api_key))
32+
}
33+
34+
/// Unified error message
35+
pub fn unauthorized(&self) -> Status {
36+
Status::unauthenticated(format!(
37+
"Request not authorized: Invalid or missing API key in {API_KEY_NAME} header"
38+
))
39+
}
40+
}
41+
42+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43+
#[serde(deny_unknown_fields)]
44+
pub struct ApiKey {
45+
pub key: String,
46+
}
47+
48+
impl ApiKey {
49+
pub fn new(key: String) -> Self {
50+
Self { key }
51+
}
52+
53+
pub fn from_str_infallible(key: &str) -> Self {
54+
Self {
55+
key: key.to_string(),
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)