Skip to content

Commit 6f164da

Browse files
committed
copy from ghmagazine#2
1 parent a19fd88 commit 6f164da

32 files changed

Lines changed: 8325 additions & 0 deletions

asyncawaitch11/log-collector/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DATABASE_URL=postgresql://postgres:password@localhost:5432/log_collector
2+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# コンパイラが生成する成果物や中間ファイルが置かれる
2+
target/

asyncawaitch11/log-collector/Cargo.lock

Lines changed: 2274 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[workspace]
2+
members = ["server", "api", "cli"]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "api"
3+
version = "0.1.0"
4+
authors = ["Rust Bicycle Book <bicycle-book@example.com>"]
5+
edition = "2018"
6+
7+
[dependencies]
8+
serde = "1.0.8"
9+
serde_derive = "1.0.8"
10+
11+
[dependencies.chrono]
12+
features = ["serde"]
13+
version = "0.4.0"
14+
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use chrono::{DateTime, Utc};
2+
use serde_derive::*;
3+
// JSONの {"user_agent": "xxx", "response_time": 0, "timestamp": "yyyy-MM-dd+HH:mm:ss"}に対応
4+
// 返り値で使うログはtimestampが`Option`ではない
5+
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
6+
pub struct Log {
7+
pub user_agent: String,
8+
pub response_time: i32,
9+
pub timestamp: DateTime<Utc>,
10+
}
11+
12+
// クエリパラメータの `?from=yyyy-MM-dd+HH:mm:ss&until=yyyy-MM-dd+HH:mm:ss` に対応
13+
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
14+
pub struct DateTimeRange {
15+
pub from: Option<DateTime<Utc>>,
16+
pub until: Option<DateTime<Utc>>,
17+
}
18+
19+
pub mod csv {
20+
pub mod get {
21+
use crate::DateTimeRange;
22+
23+
pub type Query = DateTimeRange;
24+
// getははファイルを返すのでResponse型の定義がない
25+
}
26+
27+
pub mod post {
28+
use serde_derive::*;
29+
30+
// CSVファイルを受け付けるのでリクエストデータはない
31+
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Deserialize, Serialize)]
32+
// 受領したログの数を返す
33+
pub struct Response(pub usize);
34+
}
35+
}
36+
37+
pub mod logs {
38+
pub mod get {
39+
use crate::{DateTimeRange, Log};
40+
use serde_derive::*;
41+
42+
pub type Query = DateTimeRange;
43+
44+
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Deserialize, Serialize)]
45+
// 保存しているログをすべて返す
46+
pub struct Response(pub Vec<Log>);
47+
}
48+
49+
pub mod post {
50+
use chrono::{DateTime, Utc};
51+
use serde_derive::*;
52+
53+
// 説明した通りのデータを受け付ける
54+
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Deserialize, Serialize)]
55+
pub struct Request {
56+
pub user_agent: String,
57+
pub response_time: i32,
58+
pub timestamp: Option<DateTime<Utc>>,
59+
}
60+
// Acceptedを返すのでResponseデータ型の定義はない
61+
}
62+
63+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "cli"
3+
version = "0.1.0"
4+
authors = ["Rust Bicycle Book <bicycle-book@example.com>"]
5+
edition = "2018"
6+
7+
[dependencies]
8+
clap = "2"
9+
reqwest = "0.9"
10+
csv = "1"
11+
serde = "1"
12+
serde_json = "1"
13+
api = {path = "../api"}
14+
15+
16+
[dependencies.chrono]
17+
features = ["serde"]
18+
version = "0.4"
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use clap::{App, AppSettings, Arg, SubCommand};
2+
use clap::{_clap_count_exprs, arg_enum};
3+
use reqwest::Client;
4+
use std::io;
5+
6+
arg_enum! {
7+
#[derive(Debug)]
8+
enum Format {
9+
Csv,
10+
Json,
11+
}
12+
}
13+
14+
struct ApiClient {
15+
server: String,
16+
client: Client,
17+
}
18+
19+
impl ApiClient {
20+
fn post_logs(&self, req: &api::logs::post::Request) -> reqwest::Result<()> {
21+
self.client
22+
.post(&format!("http://{}/logs", &self.server))
23+
.json(req)
24+
.send()
25+
.map(|_| ())
26+
}
27+
28+
fn get_logs(&self) -> reqwest::Result<api::logs::get::Response> {
29+
self.client
30+
.get(&format!("http://{}/logs", &self.server))
31+
.send()?
32+
.json()
33+
}
34+
35+
fn get_csv<W: io::Write>(&self, w: &mut W) -> reqwest::Result<u64> {
36+
self.client
37+
.get(&format!("http://{}/csv", &self.server))
38+
.send()?
39+
.copy_to(w)
40+
}
41+
}
42+
43+
fn do_post_csv(api_client: &ApiClient) {
44+
let reader = csv::Reader::from_reader(io::stdin());
45+
for log in reader.into_deserialize::<api::logs::post::Request>() {
46+
let log = match log {
47+
Ok(log) => log,
48+
Err(e) => {
49+
eprintln!("[WARN] failed to parse a line, skipping: {}", e);
50+
continue;
51+
}
52+
};
53+
api_client.post_logs(&log).expect("api request failed");
54+
}
55+
}
56+
57+
fn do_get_json(api_client: &ApiClient) {
58+
let res = api_client.get_logs().expect("api request failed");
59+
let json_str = serde_json::to_string(&res).unwrap();
60+
println!("{}", json_str);
61+
}
62+
63+
fn do_get_csv(api_client: &ApiClient) {
64+
let out = io::stdout();
65+
let mut out = out.lock();
66+
api_client.get_csv(&mut out).expect("api request failed");
67+
}
68+
69+
fn main() {
70+
let opts = App::new(env!("CARGO_PKG_NAME"))
71+
.about(env!("CARGO_PKG_DESCRIPTION"))
72+
.version(env!("CARGO_PKG_VERSION"))
73+
.author(env!("CARGO_PKG_AUTHORS"))
74+
// 以上がほぼテンプレート
75+
.setting(AppSettings::SubcommandRequiredElseHelp)
76+
// -s URL | --server URL のオプションを受け付ける
77+
.arg(
78+
Arg::with_name("SERVER")
79+
.short("s")
80+
.long("server")
81+
.value_name("URL")
82+
.help("server url")
83+
.takes_value(true),
84+
)
85+
// サブコマンドとして `post` を受け付ける
86+
.subcommand(SubCommand::with_name("post").about("post logs, taking input from stdin"))
87+
// サブコマンドとして `get` を受け付ける
88+
.subcommand(
89+
SubCommand::with_name("get").about("get logs").arg(
90+
Arg::with_name("FORMAT")
91+
.help("log format")
92+
.short("f")
93+
.long("format")
94+
.takes_value(true)
95+
// "csv", "json" のみを受け付ける
96+
.possible_values(&Format::variants())
97+
.case_insensitive(true),
98+
),
99+
);
100+
let matches = opts.get_matches();
101+
102+
let server = matches
103+
.value_of("SERVER")
104+
.unwrap_or("localhost:3000")
105+
// .into()が増えた
106+
.into();
107+
let client = Client::new();
108+
let api_client = ApiClient { server, client };
109+
110+
match matches.subcommand() {
111+
("get", sub_match) => {
112+
let format = sub_match
113+
.and_then(|m| m.value_of("FORMAT"))
114+
.map(|m| m.parse().unwrap())
115+
.unwrap();
116+
match format {
117+
Format::Csv => do_get_csv(&api_client),
118+
Format::Json => do_get_json(&api_client),
119+
}
120+
}
121+
("post", _) => do_post_csv(&api_client),
122+
_ => unreachable!(),
123+
}
124+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# いろいろ書かれていますが、ローカルホストの5432番ポートにユーザ名postgres、パスワードpasswordのデータベースサーバを立てる設定です
2+
postgres-data:
3+
image: busybox
4+
volumes:
5+
- /var/lib/postgresql/log-collector-data
6+
container_name: log-collector-postgres-datastore
7+
8+
postgresql:
9+
image: postgres
10+
environment:
11+
POSTGRES_USER: postgres
12+
POSTGRES_PASSWORD: password
13+
ports:
14+
- "5432:5432"
15+
volumes_from:
16+
- postgres-data
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "server"
3+
version = "0.1.0"
4+
authors = ["Rust Bicycle Book <bicycle-book@example.com>"]
5+
edition = "2018"
6+
7+
[dependencies]
8+
env_logger = "0.6"
9+
log = "0.4"
10+
actix-web = "0.7"
11+
failure = "0.1"
12+
api = {path = "../api"}
13+
dotenv = "0.13"
14+
chrono = "0.4"
15+
csv = "1"
16+
actix-web-multipart-file = "0.1"
17+
futures = "0.1"
18+
itertools = "0.8"
19+
20+
[dependencies.diesel]
21+
features = ["postgres", "chrono", "r2d2"]
22+
version = "1.4"

0 commit comments

Comments
 (0)