forked from ghmagazine/rustbook
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
32 changed files
with
8,325 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
DATABASE_URL=postgresql://postgres:password@localhost:5432/log_collector | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# コンパイラが生成する成果物や中間ファイルが置かれる | ||
target/ |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[workspace] | ||
members = ["server", "api", "cli"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
[package] | ||
name = "api" | ||
version = "0.1.0" | ||
authors = ["Rust Bicycle Book <bicycle-book@example.com>"] | ||
edition = "2018" | ||
|
||
[dependencies] | ||
serde = "1.0.8" | ||
serde_derive = "1.0.8" | ||
|
||
[dependencies.chrono] | ||
features = ["serde"] | ||
version = "0.4.0" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
use chrono::{DateTime, Utc}; | ||
use serde_derive::*; | ||
// JSONの {"user_agent": "xxx", "response_time": 0, "timestamp": "yyyy-MM-dd+HH:mm:ss"}に対応 | ||
// 返り値で使うログはtimestampが`Option`ではない | ||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] | ||
pub struct Log { | ||
pub user_agent: String, | ||
pub response_time: i32, | ||
pub timestamp: DateTime<Utc>, | ||
} | ||
|
||
// クエリパラメータの `?from=yyyy-MM-dd+HH:mm:ss&until=yyyy-MM-dd+HH:mm:ss` に対応 | ||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] | ||
pub struct DateTimeRange { | ||
pub from: Option<DateTime<Utc>>, | ||
pub until: Option<DateTime<Utc>>, | ||
} | ||
|
||
pub mod csv { | ||
pub mod get { | ||
use crate::DateTimeRange; | ||
|
||
pub type Query = DateTimeRange; | ||
// getははファイルを返すのでResponse型の定義がない | ||
} | ||
|
||
pub mod post { | ||
use serde_derive::*; | ||
|
||
// CSVファイルを受け付けるのでリクエストデータはない | ||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Deserialize, Serialize)] | ||
// 受領したログの数を返す | ||
pub struct Response(pub usize); | ||
} | ||
} | ||
|
||
pub mod logs { | ||
pub mod get { | ||
use crate::{DateTimeRange, Log}; | ||
use serde_derive::*; | ||
|
||
pub type Query = DateTimeRange; | ||
|
||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Deserialize, Serialize)] | ||
// 保存しているログをすべて返す | ||
pub struct Response(pub Vec<Log>); | ||
} | ||
|
||
pub mod post { | ||
use chrono::{DateTime, Utc}; | ||
use serde_derive::*; | ||
|
||
// 説明した通りのデータを受け付ける | ||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Deserialize, Serialize)] | ||
pub struct Request { | ||
pub user_agent: String, | ||
pub response_time: i32, | ||
pub timestamp: Option<DateTime<Utc>>, | ||
} | ||
// Acceptedを返すのでResponseデータ型の定義はない | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "cli" | ||
version = "0.1.0" | ||
authors = ["Rust Bicycle Book <bicycle-book@example.com>"] | ||
edition = "2018" | ||
|
||
[dependencies] | ||
clap = "2" | ||
reqwest = "0.9" | ||
csv = "1" | ||
serde = "1" | ||
serde_json = "1" | ||
api = {path = "../api"} | ||
|
||
|
||
[dependencies.chrono] | ||
features = ["serde"] | ||
version = "0.4" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
use clap::{App, AppSettings, Arg, SubCommand}; | ||
use clap::{_clap_count_exprs, arg_enum}; | ||
use reqwest::Client; | ||
use std::io; | ||
|
||
arg_enum! { | ||
#[derive(Debug)] | ||
enum Format { | ||
Csv, | ||
Json, | ||
} | ||
} | ||
|
||
struct ApiClient { | ||
server: String, | ||
client: Client, | ||
} | ||
|
||
impl ApiClient { | ||
fn post_logs(&self, req: &api::logs::post::Request) -> reqwest::Result<()> { | ||
self.client | ||
.post(&format!("http://{}/logs", &self.server)) | ||
.json(req) | ||
.send() | ||
.map(|_| ()) | ||
} | ||
|
||
fn get_logs(&self) -> reqwest::Result<api::logs::get::Response> { | ||
self.client | ||
.get(&format!("http://{}/logs", &self.server)) | ||
.send()? | ||
.json() | ||
} | ||
|
||
fn get_csv<W: io::Write>(&self, w: &mut W) -> reqwest::Result<u64> { | ||
self.client | ||
.get(&format!("http://{}/csv", &self.server)) | ||
.send()? | ||
.copy_to(w) | ||
} | ||
} | ||
|
||
fn do_post_csv(api_client: &ApiClient) { | ||
let reader = csv::Reader::from_reader(io::stdin()); | ||
for log in reader.into_deserialize::<api::logs::post::Request>() { | ||
let log = match log { | ||
Ok(log) => log, | ||
Err(e) => { | ||
eprintln!("[WARN] failed to parse a line, skipping: {}", e); | ||
continue; | ||
} | ||
}; | ||
api_client.post_logs(&log).expect("api request failed"); | ||
} | ||
} | ||
|
||
fn do_get_json(api_client: &ApiClient) { | ||
let res = api_client.get_logs().expect("api request failed"); | ||
let json_str = serde_json::to_string(&res).unwrap(); | ||
println!("{}", json_str); | ||
} | ||
|
||
fn do_get_csv(api_client: &ApiClient) { | ||
let out = io::stdout(); | ||
let mut out = out.lock(); | ||
api_client.get_csv(&mut out).expect("api request failed"); | ||
} | ||
|
||
fn main() { | ||
let opts = App::new(env!("CARGO_PKG_NAME")) | ||
.about(env!("CARGO_PKG_DESCRIPTION")) | ||
.version(env!("CARGO_PKG_VERSION")) | ||
.author(env!("CARGO_PKG_AUTHORS")) | ||
// 以上がほぼテンプレート | ||
.setting(AppSettings::SubcommandRequiredElseHelp) | ||
// -s URL | --server URL のオプションを受け付ける | ||
.arg( | ||
Arg::with_name("SERVER") | ||
.short("s") | ||
.long("server") | ||
.value_name("URL") | ||
.help("server url") | ||
.takes_value(true), | ||
) | ||
// サブコマンドとして `post` を受け付ける | ||
.subcommand(SubCommand::with_name("post").about("post logs, taking input from stdin")) | ||
// サブコマンドとして `get` を受け付ける | ||
.subcommand( | ||
SubCommand::with_name("get").about("get logs").arg( | ||
Arg::with_name("FORMAT") | ||
.help("log format") | ||
.short("f") | ||
.long("format") | ||
.takes_value(true) | ||
// "csv", "json" のみを受け付ける | ||
.possible_values(&Format::variants()) | ||
.case_insensitive(true), | ||
), | ||
); | ||
let matches = opts.get_matches(); | ||
|
||
let server = matches | ||
.value_of("SERVER") | ||
.unwrap_or("localhost:3000") | ||
// .into()が増えた | ||
.into(); | ||
let client = Client::new(); | ||
let api_client = ApiClient { server, client }; | ||
|
||
match matches.subcommand() { | ||
("get", sub_match) => { | ||
let format = sub_match | ||
.and_then(|m| m.value_of("FORMAT")) | ||
.map(|m| m.parse().unwrap()) | ||
.unwrap(); | ||
match format { | ||
Format::Csv => do_get_csv(&api_client), | ||
Format::Json => do_get_json(&api_client), | ||
} | ||
} | ||
("post", _) => do_post_csv(&api_client), | ||
_ => unreachable!(), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# いろいろ書かれていますが、ローカルホストの5432番ポートにユーザ名postgres、パスワードpasswordのデータベースサーバを立てる設定です | ||
postgres-data: | ||
image: busybox | ||
volumes: | ||
- /var/lib/postgresql/log-collector-data | ||
container_name: log-collector-postgres-datastore | ||
|
||
postgresql: | ||
image: postgres | ||
environment: | ||
POSTGRES_USER: postgres | ||
POSTGRES_PASSWORD: password | ||
ports: | ||
- "5432:5432" | ||
volumes_from: | ||
- postgres-data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[package] | ||
name = "server" | ||
version = "0.1.0" | ||
authors = ["Rust Bicycle Book <bicycle-book@example.com>"] | ||
edition = "2018" | ||
|
||
[dependencies] | ||
env_logger = "0.6" | ||
log = "0.4" | ||
actix-web = "0.7" | ||
failure = "0.1" | ||
api = {path = "../api"} | ||
dotenv = "0.13" | ||
chrono = "0.4" | ||
csv = "1" | ||
actix-web-multipart-file = "0.1" | ||
futures = "0.1" | ||
itertools = "0.8" | ||
|
||
[dependencies.diesel] | ||
features = ["postgres", "chrono", "r2d2"] | ||
version = "1.4" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# For documentation on how to configure this file, | ||
# see diesel.rs/guides/configuring-diesel-cli | ||
|
||
[print_schema] | ||
file = "src/schema.rs" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
use crate::model::*; | ||
use chrono::{DateTime, Utc}; | ||
use diesel::insert_into; | ||
use diesel::prelude::*; | ||
use diesel::result::QueryResult; | ||
|
||
pub fn insert_log(cn: &PgConnection, log: &NewLog) -> QueryResult<i64> { | ||
use crate::schema::logs::dsl; | ||
insert_into(dsl::logs) | ||
.values(log) | ||
.returning(dsl::id) | ||
.get_result(cn) | ||
} | ||
|
||
pub fn insert_logs(cn: &PgConnection, logs: &[NewLog]) -> QueryResult<Vec<i64>> { | ||
use crate::schema::logs::dsl; | ||
insert_into(dsl::logs) | ||
.values(logs) | ||
.returning(dsl::id) | ||
.load(cn) | ||
} | ||
|
||
pub fn logs( | ||
cn: &PgConnection, | ||
from: Option<DateTime<Utc>>, | ||
until: Option<DateTime<Utc>>, | ||
) -> QueryResult<Vec<Log>> { | ||
use crate::schema::logs::dsl; | ||
|
||
// 型エラーを防ぐためにinto_boxedを呼んでおく | ||
let mut query = dsl::logs.into_boxed(); | ||
if let Some(from) = from { | ||
query = query.filter(dsl::timestamp.ge(from.naive_utc())) | ||
} | ||
if let Some(until) = until { | ||
query = query.filter(dsl::timestamp.lt(until.naive_utc())) | ||
} | ||
query.order(dsl::timestamp.asc()).load(cn) | ||
} |
Oops, something went wrong.