【1日目】graqt開発日誌 ~GolangでリッチなCLIを作る~

最近ブログ書いてないので、graqtの進捗を1mmでも進んだら書くことにする。

graqtについては、以下の記事を参考にされたし。 一言で言うと、バカでも使えるGolang用のWebアプリのボトルネックを調査するソフトウェアです。主にISUCONをターゲットにしています。

serinuntius.hatenablog.jp github.com

進捗

現在、CLI部分を作っていて、見た目だけ少し動くようになった。 まだ、ログ部分とつなぎこみはしていない。

別日だけど、sketchでワイヤー作ったりもした。

golangでリッチなCLIを作るには

jroimartin/gocuiというOSSがすごく便利です。

_exampleに参考になるコードが結構ある。 github.com

今日のコードを貼っておきます。 汚いのは自覚してるし、graqtに持っていくときは清書しますw

github.com

package main

import (
    "log"

    "github.com/jroimartin/gocui"
    "fmt"
    "text/tabwriter"
    "github.com/pkg/errors"
)

type RequestWidget struct {
    name    string
    x, y    int
    w, h    int
    counter int
    v       *gocui.View
    tw      *tabwriter.Writer
}

func NewRequestWidget(name string, x, y, w, h int) *RequestWidget {
    // add initialize
    return &RequestWidget{name: name, x: x, y: y, w: w, h: h, counter: 0}
}

func (w *RequestWidget) Layout(g *gocui.Gui) error {
    // _ => vgit
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Request Index"
        v.Highlight = true
        v.SelBgColor = gocui.ColorGreen
        v.SelFgColor = gocui.ColorBlack

        v.SetCursor(0, 1)

        if err := w.KeyBindings(g); err != nil {
            return errors.Wrap(err, "Failed to Set KeyBindings")
        }

        if _, err := g.SetCurrentView("request"); err != nil {
            log.Panicln(err)
        }

        w.InitTabWriter(v)

        if _, err := w.PrintHeader(); err != nil {
            return err
        }

        // TODO テスト用
        for i := 0; i < 10; i++ {
            fmt.Fprintln(w.tw, "\t300\tGET\t/hoge\t10\t0.3\t5\t3000\t3\t8\t9\t8\t300mb\t50mb\t30mb\t30000mb")
        }

        w.tw.Flush()
        fmt.Fprintln(w.v)
    }

    return nil
}

func (w *RequestWidget) InitTabWriter(v *gocui.View) error {
    w.v = v
    w.tw = tabwriter.NewWriter(v, 0, 8, 2, ' ', 0)
    return nil
}

func (w *RequestWidget) PrintHeader() (int, error) {
    return fmt.Fprintln(w.tw, "\tCount\tMethod\tPath\tMax\tMin\tAvg\tSum\tP1\tP50\tP99\tStddev\tMaxBody\tMinBody\tAvgBody\tSumBody")
}

func (w *RequestWidget) enter(g *gocui.Gui, v *gocui.View) error {
    w.v.Clear()
    w.PrintHeader()
    w.counter++
    for i := 0; i < w.counter; i++ {
        fmt.Fprintln(w.tw, "\taaaaaaaaaa\tbbb")
    }

    w.tw.Flush()
    fmt.Fprintln(w.v)

    return nil
}

func (w *RequestWidget) cursorUp(g *gocui.Gui, v *gocui.View) error {
    if v != nil {
        ox, oy := v.Origin()
        cx, cy := v.Cursor()

        // cy == 0 is header
        if cy == 1 {
            return nil
        }
        if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
            if err := v.SetOrigin(ox, oy-1); err != nil {
                return err
            }
        }
    }
    return nil
}

func (w *RequestWidget) cursorDown(g *gocui.Gui, v *gocui.View) error {
    if v != nil {
        cx, cy := v.Cursor()
        if err := v.SetCursor(cx, cy+1); err != nil {
            ox, oy := v.Origin()
            if err := v.SetOrigin(ox, oy+1); err != nil {
                return err
            }
        }
    }
    return nil
}

func (w *RequestWidget) Printf(format string, a ...interface{}) {
    fmt.Fprintf(w.tw, format, a)
    w.tw.Flush()
    fmt.Fprintln(w.v)
}

type HelpWidget struct {
    name string
    x, y int
    w, h int
    body string
}

func NewHelpWidget(name string, x, y, w, h int) *HelpWidget {
    // add initialize
    return &HelpWidget{name: name, x: x, y: y, w: w, h: h}
}

func (w *HelpWidget) Layout(g *gocui.Gui) error {
    // _ => v
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Help"
        fmt.Fprintf(v, "%d:%d:%d:%d", w.x, w.y, w.x+w.w, w.y+w.h)
    }
    return nil
}

type QueryWidget struct {
    name string
    x, y int
    w,

    h int
}

func NewQueryWidget(name string, x, y, w, h int) *QueryWidget {
    // add initialize
    return &QueryWidget{name: name, x: x, y: y, w: w, h: h}
}

func (w *QueryWidget) Layout(g *gocui.Gui) error {
    // _ => v
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Query Index"
    }
    return nil
}

type ParameterWidget struct {
    name string
    x, y int
    w, h int
}

func NewParameterWidget(name string, x, y, w, h int) *ParameterWidget {
    // add initialize
    return &ParameterWidget{name: name, x: x, y: y, w: w, h: h}
}

func (w *ParameterWidget) Layout(g *gocui.Gui) error {
    // _ => v
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Query Parameter"
    }
    return nil
}

func quit(g *gocui.Gui, v *gocui.View) error {
    return gocui.ErrQuit
}

func (w *RequestWidget) KeyBindings(g *gocui.Gui) error {
    if err := g.SetKeybinding("request", gocui.KeyEnter, gocui.ModNone, w.enter); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyArrowDown, gocui.ModNone, w.cursorDown); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyArrowUp, gocui.ModNone, w.cursorUp); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyCtrlN, gocui.ModNone, w.cursorDown); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyCtrlP, gocui.ModNone, w.cursorUp); err != nil {
        return err
    }

    return nil
}

func main() {
    g, err := gocui.NewGui(gocui.OutputNormal)
    if err != nil {
        log.Panicln(err)
    }
    defer g.Close()

    g.Highlight = true
    g.SelFgColor = gocui.ColorBlue

    winX, winY := g.Size()
    fmt.Println(winX, winY)

    request := NewRequestWidget("request", 0, 0, winX-1, winY-3)
    help := NewHelpWidget("help", 0, winY-3, winX-1, 2)
    //query := NewQueryWidget("query", winX/2, 0, winX/2-1, winY-3)
    //parameter := NewParameterWidget("parameter", winX/2, winY/2, winX/2-1, winY/2-2)

    g.SetManager(help, request) //, query, parameter)

    if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
        log.Panicln(err)
    }

    if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
        log.Panicln(err)
    }
}

【自宅鯖】自作PCの電源を換装する【オウルテック Seasonic SSR-650FM】

f:id:serinuntius:20180721195222p:plain

6月の頭ぐらいから、ゲーミングPCとして利用していたタワー型の自作デスクトップを、サーバとして利用していた。

Mackerelで監視していたのだが、しばらくは安定して稼働していた。

しかし、ここ2週間ぐらいで不安定になってきた。ログとか見ても特に変なところがない。 連日の猛暑のせいで熱暴走しているっていう線も考えたけど、ほとんどCPU等はアイドルだし、温度を見てみてもそこまで異常値ではなかった。

このPCを組んでもう5年ぐらい経つので電源の寿命ではないかと思い、Amazonでポチって、サクッと換装したレポートだ。

古い電源を取り外す

こんな感じのタワーだ。今から掃除するので寝かせている。 サイドパネルをサクッと開ける。 f:id:serinuntius:20180721092455j:plain

昔の電源はSilverStone ST75F-Pだ。 750wのフルプラグイン式で80 Plus Silverだ。

別に悪くなかったように思う、ただ単に寿命なのだろう。

f:id:serinuntius:20180721092732j:plain

電源が刺さっているところ一覧

EATXPWR 24Pin

f:id:serinuntius:20180721095410p:plain

EATX 8Pin

見にくいけど、マザボの左上にある。 f:id:serinuntius:20180721094959p:plain

グラボ PCIe 6Pin * 2

f:id:serinuntius:20180721095007p:plain

ケースファン ペリフェラル4Pin * 2

ここでケースの右サイドのパネルを開ける。 配線汚い😇

f:id:serinuntius:20180721095454j:plain

こいつが、ケースのファンの電源といろいろ繋がっている。 f:id:serinuntius:20180721095944p:plain

絵面は似てるけどもう1つ。 f:id:serinuntius:20180721100208p:plain

HDD SATA15Pin

f:id:serinuntius:20180721100517p:plain

SSD SATA15Pin

Windows用のSSD。5年前のもので、128GB。 f:id:serinuntius:20180721100645p:plain

SSD2 SATA15Pin

サーバ用のSSD。1ヶ月ぐらい前に買った、256GB。

f:id:serinuntius:20180721100937p:plain

DVDドライブ SATA15Pin

f:id:serinuntius:20180721101100p:plain

全部抜いて外に出す

電源ケーブルの抜き忘れがないように、ケースの外にケーブルを全部出して、全部抜いたかを確認する。

f:id:serinuntius:20180721101429j:plain

電源を固定しているネジを緩めて外す

4箇所ネジで止まっているので外す。 f:id:serinuntius:20180721101722p:plain

ホコリが少し溜まっている。 f:id:serinuntius:20180721101810j:plain

プラグイン式の電源は使うものだけ挿せばいいので、電源の前に使わないケーブルをまとめておかないくていいのでオススメである。 f:id:serinuntius:20180721101858j:plain

掃除

ケースは、今はなきZalmanのZ12 Plus

フロントパネルをガバッと外す。結構硬い。 f:id:serinuntius:20180721102137j:plain

フロントパネルのファン。汚い。 f:id:serinuntius:20180721102153j:plain

フロントパネルに付いてる、フィルターを取ってみる。 f:id:serinuntius:20180721102218j:plain

うげぇ〜、汚い。 f:id:serinuntius:20180721102220j:plain

ケース底部のも掃除しておく。 f:id:serinuntius:20180721102247j:plain

ここでちらっと写った掃除機は、ダイソンのコードレス。 一人暮らしするときに、母に強くダイソンの掃除機をオススメされて購入に至った。 f:id:serinuntius:20180721102406j:plain

新しい電源

f:id:serinuntius:20180721102452j:plain

新しい電源はオウルテック Seasonic SSR-650FMを選択した。

この電源を選択した理由

  • 信頼できるメーカー
  • 驚きの7年間保証
  • 80 Plus Gold
  • サブプラグイン

電源容量は、50% のときの一番効率よく変換できるので、50% + αになるように組む。

電源容量の見積もりは、電源容量計算(電源電卓)電源の選び方|ドスパラ通販【公式】 が大変参考になる。

f:id:serinuntius:20180721102458j:plain 開封の儀。じゃ〜ん。

電源に小物入れ(?)がついていてびっくりした。 f:id:serinuntius:20180721102529j:plain

電源は、メイン以外はプラグイン式になっている。 電源コードがわかりにくいけど、平麺みたいになっていて薄くて配線しやすかった。ベビースタードデカイラーメンみたいと言えばわかりやすいだろうか。 f:id:serinuntius:20180721102606j:plain

仮組み

意図せず、ブラックとゴールドが揃っていてかっこいい。 f:id:serinuntius:20180721115813j:plain

EATXPWR 24Pin

f:id:serinuntius:20180721115847j:plain

プラグインペリフェラル4Pin

f:id:serinuntius:20180721115918j:plain

プラグインSATA 15Pin

f:id:serinuntius:20180721115930j:plain

【秘技】空中マウント

良い子は真似しない。 f:id:serinuntius:20180721115944j:plain

メモリテスト

ここで、サイドパネルとか開けたまま、メモリテストを回してみる。

電源のピンの刺し忘れや、メモリがちゃんと認識するかのテスト。*1

今回初めてメモリテストをしたが、memtest86+というのが定番らしい。

grub2から起動すると、何もしなくてもテストが回り始める。 このテストは自動的に何回もループするようで、3回ぐらい回すのがいいらしい。 f:id:serinuntius:20180721120106j:plain

一晩放置して、Pass: 4, Errors: 0 と出ていたので、メモリは大丈夫そう。 f:id:serinuntius:20180721192223p:plain

サイドパネルを閉めていく

組み立てているときに、今回このPCをサーバ用と割り切ることにして、HDDやWin用のSSDやGTX 970も取り付けるのをやめた。

安定性が増すことや、アイドル時の電気代を加味するとその方が良い選択な気がした。(が、今回650wを選択した意味がなくなってしまった。)

f:id:serinuntius:20180721192451j:plain

まとめ

換装前は半日で落ちていたサーバであったが、今回の電源の換装で安定してサーバが稼働するようになった。

完全に結果論だが、電源の寿命という推察はだいたい当たっていた。

*1:本当はメモリが原因か、電源が原因か見極めるため、電源交換前にもやるべきだった。

【Golang】GitHubのAPIでIssueのコメントを全件取ってくるスクリプト

f:id:serinuntius:20180720231919p:plain

昨日モテるシェル芸とか言って、ブログ書いてたんですけど、

serinuntius.hatenablog.jp

実はGitHubのIssueのコメントが大量すぎて、Load more になっちゃってて、全部の画像をダウンロードできてなかったみたいです。

f:id:serinuntius:20180720133258p:plain

それをデザイナーに指摘されて、全部取ってこようと思ってGitHubのAPI見てたら、Headerに次のページのlinkが入っているという仕様で、シェルスクリプト書くぐらいならGoで書きたいな〜と思ってGoで書きました。

GitHubのTokenを取得する

GitHubのパスワード認証だけだったらわざわざToken取得しなくてもいいんですけど、2段階認証している方はTokenの取得が必要となってきます。

https://github.com/settings/tokens にアクセスして、Tokenを取得します。

スクリプト

GoのGitHub APIのクライアントは

github.com

が便利です。

gist.github.com

使い方

環境変数TOKENにGitHubのTokenを入れます。

Passwordをベタ打ちしない方法は、Qiitaに書きました。 qiita.com

あと、constのrepoとかownerは自分のものに変えてください。

read -s 'TOKEN?tokenを入れてください>'
go run main.go | xargs wget

上記コマンドでIssueの画像が全件ダウンロードされるはずです。 されないときは、正規表現等が怪しいと思うので、その辺りを修正してください。

まとめ

これで、デザイナーから「おい!全部じゃねえぞ」ってツッコミが飛んでこなくなりましたね。*1

めでたしめでたし。

*1:実際にはそんなツッコミは飛んできていません。

【裏技】みんな知らないログイン必須ページの爆速スクレイピング【モテるシェル芸】

f:id:serinuntius:20180718233358p:plain

おはようございます。

裏技ってつけると急にワザップ感が出て、懐かしいですよね〜。 こないだ飲み会で同期とそんな話をしておりました。

本題

ログインが必要なWebサイトで画像を引っこ抜いて欲しいという依頼があり、スクリプトを書くかな〜と迷ったんですが、よく考えたらシェル芸だけで出来るな〜と思ったので共有したいと思います。

今回はデザイナーにGitHubのIssueに貼ってある画像200枚以上をzipで欲しいって言われたので、それを題材にします。

環境

やり方

1. Chromeでおもむろにデベロッパーツールを開く

Macなら Shift + Cmd + c等で開けます。

2. networkを選択する

f:id:serinuntius:20180718222407p:plain

そのページのリクエストを見つける たぶん、一番上のはず。

f:id:serinuntius:20180718222514p:plain

3. 右クリックして、Copy as cURLを選択

f:id:serinuntius:20180718222603p:plain

今回の肝はこれで、ブラウザで送ったリクエストと全く同じリクエストをcurlで再現できるようになっております。 つまり、cookieの情報等も渡してくれるので、認証が通った状態でアクセス出来るわけですね。

GitHubのプライベートリポジトリは、認証してない状態でアクセスすると404を返してくるのですがそれを回避できます。

4. お好きなターミナルにコピペ

わかりにくいですが、めっちゃ長いcurlコマンドが貼り付けられてます。 f:id:serinuntius:20180718224134j:plain

5. いい感じにシェル芸してく

今回は、Issueに投稿された画像を抜いてきたいので、こんな感じになりました。

curl github.com/.../../ -H '....' \ 
|grep '<img' \
|egrep -o "https://user.*?\.(png|jpg)"\
|uniq\
|xargs wget

一応説明しておくと、

  1. grep '<img'で当たりをつける
  2. grepの拡張版であるegrepで正規表現を使っていい感じに抜き取る (oオプションはマッチした場所だけ取ってくる便利なオプションです。egrepじゃなくて、grepのEオプションでも可)
  3. 何個か重複してたりしたので、uniqをかませる
  4. xargsでwgetにurlを渡す (ここで意外なのが、画像には認証が不要というとところです。 uuid振ってあるから、大丈夫という認識なのでしょうか?)

まとめ

これを頼まれて5分ぐらいでサクッとやって「出来たよ」って言って渡せば、モテること間違いなしですね!!!!

f:id:serinuntius:20180718225759p:plain

(念の為言っておきますがこれでモテていると勘違いしているわけではありません。24年生きてるので、それぐらいわかっているつもりです。)

dockerのコンテナを楽に選択するpecoを使ったalias

ブログ書くの怠ってたから、カジュアルにアウトプットしていくぞ!!!

本題

環境

  • peco
  • zsh(zshのグローバルエイリアスという機能を使っているみたいなのでzshしかできないと思います。)
  • docker

手順

~/.zshrc に以下を書くだけ

alias -g C='`docker ps -a|peco| cut -d" " -f 1`'

使い方

  • docker logs C
  • dcoker exec -it C bash

これぐらいしか思いつかないけど、まあ便利

go run main.goとすると別ファイルのmainパッケージのグローバル関数がundefinedで怒られる

めっちゃ久しぶりのブログになってしまった。

書くことなんか何個でもあるのに、バタバタしてたりでアウトプットを疎かにしていた。(言い訳)

本題

少し前から気になってたことだったんだけど、go run main.go で実行すると、mainパッケージの別ファイルのグローバルな関数の呼び出しができない。

github.com

最小構成のリポジトリを作った。

これを、適当にcloneして、 go run main.go すると実行できない。

解決策

buildする。 go build -o main && ./main で buildすると実行できる。

どうしてもgo runしたいときの解決策

1. go run main.go lib.go ファイルを列挙する

ルートにファイルが増えると面倒なので、できたらやめたほうがいいと思う。

2. go run *.go *でごまかす

この方法は楽なんだけど、テストのファイル(例) main_test.go)があると破綻する。

同じようにハマってる人がいた

stackoverflow.com

この件で不思議なのは、main以外のパッケージだと普通に読み込めるところ。 例えば、こういう構成だったら大丈夫。

root/
  --- main.go
  --- lib/
  ------ hello.go

この場合は import "host/username/root/lib" というふうにimport文を書くからそこから解決して実行できるんだろうな(想像)

これ、初めて遭遇したときはびっくりするんだよな。

kamakura.go #4 に登壇しました!

昨日(2018年5月25日)行われた、kamakura.goのLTで以前から書いているGoのパッケージであるgraqt(がらくた)の紹介をしてきました。

前回のgraqtの記事

serinuntius.hatenablog.jp

発表資料

speakerdeck.com

登壇後に質問されたこととか

(あいまいな)記憶と #kamakurago のハッシュタグを辿りながら書いてます。

Q. トレーサーのオーバーヘッドは? 5%以下の性能低下なら、良いよねってなんかの本で書いてたよ〜。

A. 実際、オーバーヘッドはあります。以前検証したベンチマークでは、以下のようなことがわかっています。

1リクエストで発行クエリが増えれば増えるほど、オーバーヘッドは大きくなる。(当たり前ですが)

1リクエストで、1クエリーだった場合のオーバーヘッドは、

  • ログあり1547.08 Requests/sec
  • ログなし1607.31 Requests/sec

1547.08 / 1607.31 * 100 = 96.25274527 となり、5%以下になっている。

しかし、1リクエストで、100クエリーだった場合のオーバーヘッドは、

  • ログあり 58.89 Requests/sec
  • ログなし 54.56 Requests/sec

で、

54.56 / 58.89 * 100 = 92.6473085413 となり、5%を超えてしまっている。

便利そうですね。応援しています。

ありがとうございます。そういってもらえて嬉しいです。

まとめ

今後もGo書いてくぞ。