仕事のがんばり具合を記録して可視化する(記録する編)
はじめに
これは Yahoo! JAPAN 18 新卒 Advent Calendar 2018 の21日目の記事です。
みなさん仕事がんばってますか?自分は新卒らしくがんばっています。がんばっているので、がんばり具合を記録して可視化します。
やりたいことはだいたいこういうことです。 pokutuna.hatenablog.com
ざっくり書くと:
- 開いているウィンドウのログを記録する
- グラフにする
ということをやります。どんなウィンドウを開いていたかを時系列に並べてグラフにすれば「10時から1時間くらい Emacs 開いてるからコード書いてるんだな、がんばってるっぽいぞ!」というのがわかります。これががんばり具合です。
セキュリティ的に、社外秘を含む情報を扱う端末のログの外部送信は問題があるので、このような取り組みを社内の環境で行えるような仕組みを自分で作ります。今回は記録するところについて書きます。
できたもの
社用 PC が Mac なので Cocoa でアプリケーションを作ります。Swift は書いたことがないのでインターネットを駆使してがんばります。がんばると、このような JSON が獲得できます。
[ { "kCGWindowAlpha" : 1, "kCGWindowLayer" : 0, "kCGWindowMemoryUsage" : 1128, "kCGWindowSharingState" : 0, "kCGWindowOwnerPID" : 39418, "kCGWindowNumber" : 154478, "kCGWindowOwnerName" : "Xcode", "kCGWindowStoreType" : 1, "kCGWindowBounds" : { "X" : 135, "Height" : 877, "Y" : 23, "Width" : 1305 }, "kCGWindowName" : "AppDelegate.swift" }, { "kCGWindowAlpha" : 1, "kCGWindowLayer" : 0, "kCGWindowMemoryUsage" : 1128, "kCGWindowSharingState" : 1, "kCGWindowOwnerPID" : 2160, "kCGWindowNumber" : 151228, "kCGWindowOwnerName" : "Google Chrome", "kCGWindowStoreType" : 1, "kCGWindowBounds" : { "X" : 170, "Height" : 62, "Y" : 92, "Width" : 366 }, "kCGWindowName" : "" }, ...
開いているウィンドウの名前と、どのアプリケーションから起動されているかがわかりますね。おおむね重なり順も合っているはずですが、雑に作ったので怪しいかもしれない。 JSON が得られたので、Elasticsearch + Kibana で可視化できそうな感じがしますね。
しくみ
常駐の Cocoa App で5秒ごとにウィンドウログ取得して、JSON 形式で保存させています。XCode の気持ちになって、しかるべき場所にこのようなコードが書いてあると思ってください。
func applicationDidFinishLaunching(_ aNotification: Notification) { Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(self.saveLog), userInfo: nil, repeats: true) } @objc func saveLog() -> Void { let filename = self.getDocumentsDirectory().appendingPathComponent("\(NSDate().timeIntervalSince1970).json") let windowList = self.getWindowList() let data: Data = try! JSONSerialization.data(withJSONObject: windowList!, options: []) do { let windowInfoList = try JSONDecoder().decode([MyWindowInfo].self, from: data) let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let encoded = try! encoder.encode(windowInfoList) do { try String(data: encoded, encoding: .utf8)!.write(to: filename, atomically: true, encoding: String.Encoding.utf8) } catch { print(error) } } catch { print(error) } }
getWindowList
と MyWindowInfo
あたりが大事なので、詳しく説明します。
ウィンドウリストを取得する
CGWindowListCopyWindowInfo()
でさっくりウィンドウリストが取得できます。CFArray で結果が返ってくるので、外側は NSArray にキャストします。中身は NSDictioanry とします。.optionAll
ですべてのウィンドウを取ってくると常駐アプリケーションや Desktop プロセスなどのがんばり具合に寄与しないものが取れてしまうので、適当にフィルタします。高さが23以下のウィンドウは常駐アプリケーションで、layer が 0 より小さいのは Desktop プロセスとかその辺です。用途によっては自作のフィルタではなく、CGWindowListCopyWindowInfo()
のオプションでいい感じに必要なウィンドウだけ取得することもできます。
func getWindowList() -> [NSDictionary]? { guard let windowList: NSArray = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) else { return nil } let swiftWindowList = windowList as! [NSDictionary] let filteredWindowList = swiftWindowList.filter { (windowIndo: NSDictionary) -> Bool in var flag: Bool = true // Bounds filter let bounds = windowIndo[kCGWindowBounds] as! NSDictionary let height = bounds["Height"] as! Int if height <= 23 { flag = false } // Layer filter let layer = windowIndo[kCGWindowLayer] as! Int if layer < 0 { flag = false } return flag } return filteredWindowList }
JSON に変換する
getWindowList()
で得られるのは NSDictionary の NSArray なので、JSON 文字列に変換していきます。
JSONSerialization -> JSONDecode -> JSONEncode という手順でやります。
Serialization で潰したデータを一旦 Decode して、人間に優しい表記で Encode する感じです。
平たく書くとこうなっています。例外の気持ちになって、しかるべきコードに読み替えてください。
let windowList = self.getWindowList() // -> [NSDictionary] // windowList を1行に潰す let data: Data = JSONSerialization.data(withJSONObject: windowList, options: []) // ねじ込む let windowInfoList = JSONDecoder().decode([MyWindowInfo].self, from: data) // 人間に優しい表記にする let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let encoded = encoder.encode(windowInfoList) let dst = String(data: encoded, encoding: .utf8)!
Decode するときは Codable
を使います。潰したデータを任意の構造体にねじ込めます。便利ですね。
struct MyWindowInfo: Codable { var kCGWindowAlpha: Double var kCGWindowBounds: MyWindowBounds var kCGWindowIsOnscreen: Bool? var kCGWindowLayer: Int var kCGWindowMemoryUsage: Int var kCGWindowName: String? var kCGWindowNumber: Int var kCGWindowOwnerName: String var kCGWindowOwnerPID: Int var kCGWindowSharingState: Int var kCGWindowStoreType: Int struct MyWindowBounds: Codable { var Height: Int var Width: Int var X: Int var Y: Int } }
ログについて
ログは溜めて使ってこそ価値がありますね。弊社もそのようにやっています。そういうわけで、運用にあたって調べておきたいことを調べておきます。
容量・転送
常駐アプリケーションで5秒毎にテキストデータを生成しているので、ログが気になります。見てみると1ファイルあたり 44KB でした。
1時間で 60 * 60 / 5 * 44 = 31680KB
になります。32MB くらいですね。今月の必要労働時間は147時間15分らしいので、1ヶ月で (147 * 60 * 60 + 15 * 60) / 5 * 44 = 4664880KB
になります。4.7GB くらいですね。
保存期間を1年間とすると 56GB くらいになります。現実的な容量ですね。
また、ログの保存と可視化のために、適当なサーバに定期的に転送する必要があります。 1時間ごとに転送するとして 32MB くらいなので、現代のインターネットなら問題なさそうです。 社内のネットワークを計測したところ、7.80MB/sec でした。5,6秒あれば転送できそうです。
メモリ使用量
だいたい 12MB くらい食べています。社用 PC のメモリは 16GBなので、常駐させても問題なさそうです。
セキュリティ
ウィンドウ名に社外秘情報が含まれる可能性があるので、生ログは社内にしまっておく必要があります。
まとめ
いつ、どのくらい、どんなウィンドウ(アプリケーション)を開いていたかのログを使って、仕事のがんばり具合を記録・可視化する取り組みについて書きました。 この記事では Cocoa でロガーを作るところと、ログの保管について少しだけ検証しました。可視化するところは作業中なので、また別の記事に書きます。
Swift は初めて書いたので、記事内のソースコードについてコメントがあればぜひお願いします。また、このプログラムは社内の GHE に置いているので、興味のある方は入社して一緒にがんばり具合を記録しましょう。
以上、Yahoo! JAPAN 18 新卒 Advent Calendar 2018 の21日目の記事でした。
参考
- Mackerel で仕事のがんばり具合を見える化する - ポクポク
- デスクトップ上にあるウインドウの一覧を取得する - Qiita
- CGWindowListCopyWindowInfo(::) - Core Graphics | Apple Developer Documentation
- Archives and Serialization | Apple Developer Documentation
- JSONDecoder - Foundation | Apple Developer Documentation
- Codableで色々なJSONに対応する - Qiita
- scheduledTimer(timeInterval:target:selector:userInfo:repeats:) - Timer | Apple Developer Documentation
- How to save a string to a file on disk with write(to:) - free Swift 4.2 example code and tips
- Get Unix Epoch Time in Swift - Stack Overflow
- ステータスバー常駐アプリ: Cocoa日曜プログラム日誌 ここぶろ