Skip to content

Commit

Permalink
copy from ghmagazine#2
Browse files Browse the repository at this point in the history
  • Loading branch information
denjiry committed Dec 29, 2019
1 parent a19fd88 commit 6f164da
Show file tree
Hide file tree
Showing 32 changed files with 8,325 additions and 0 deletions.
2 changes: 2 additions & 0 deletions asyncawaitch11/log-collector/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DATABASE_URL=postgresql://postgres:password@localhost:5432/log_collector

2 changes: 2 additions & 0 deletions asyncawaitch11/log-collector/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# コンパイラが生成する成果物や中間ファイルが置かれる
target/
2,274 changes: 2,274 additions & 0 deletions asyncawaitch11/log-collector/Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions asyncawaitch11/log-collector/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[workspace]
members = ["server", "api", "cli"]
14 changes: 14 additions & 0 deletions asyncawaitch11/log-collector/api/Cargo.toml
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"

63 changes: 63 additions & 0 deletions asyncawaitch11/log-collector/api/src/lib.rs
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データ型の定義はない
}

}
18 changes: 18 additions & 0 deletions asyncawaitch11/log-collector/cli/Cargo.toml
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"
124 changes: 124 additions & 0 deletions asyncawaitch11/log-collector/cli/src/main.rs
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!(),
}
}
16 changes: 16 additions & 0 deletions asyncawaitch11/log-collector/docker-compose.yml
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
22 changes: 22 additions & 0 deletions asyncawaitch11/log-collector/server/Cargo.toml
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"
5 changes: 5 additions & 0 deletions asyncawaitch11/log-collector/server/diesel.toml
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.
39 changes: 39 additions & 0 deletions asyncawaitch11/log-collector/server/src/db.rs
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)
}
Loading

0 comments on commit 6f164da

Please sign in to comment.