Mackerel DSLを作ろう

できました、こちらです(一部抜粋)。

mackerel {
    apiKey ".........."

    host {
        name "foo"

        agent "mackerel-agent/0.78.3 (Revision f41be78)" "f41be78" "0.78.3"
        memory [ "total", "33554432kB"; "something", "useful" ]
    }

    host {
        name "bar"
        memo "hoge"
    }
}

おっと、こちらは はてなエンジニア Advent Calendar 2023 の42日目です。 昨日は id:lufiabb さんによる Mackerelのラベル付きメトリック実装でentを使ってスキーマを書いてみた話 - Mackerel お知らせ #mackerelio でした。

developer.hatenastaff.com

さて先ほどのコードは、ぱっと見ではRubyに見えなくもないですが、構文的に怪しそうなところがありますね。 そんなわけでお察しの通りいつものF#です。 内部DSLなのでこのままdotnet runで実行できます。

F#ではコンピュテーション式のカスタムオペレータってやつを使うとこんなことが実現できます。 しかも静的型付けなので定義も利用も間違ってるとコンパイルエラーが出るので安心です。

以前 にも利用した FsHttp などもこれを上手く使っていて、非常に参考になります。

では実装を見ていきましょう。 こちらも全部網羅すると大変なので、というかまだそこまで作り込んではいないので、一部のみ掲載します。

まずは基本の型から。 ここは普通ですね。

type HostMetadata =
    | Agent of name: string * revision: string * version: string
    | Memory of (string * string) list

type Host =
    { Name: string
      Memo: string option
      Metadata: HostMetadata list }

type Mackerel = { ApiKey: string; Hosts: Host list }

で、コンピュテーション式のキモとなるビルダーを定義。 まだ全容を把握しているわけではないけど、特定のメソッドを実装することでコンピュテーション式の構文に変換されるようです。 ちなみにXxxProperty型はコンピュテーション式とは直接の関係は無く、実装上で便利になるので定義しています。

[<RequireQualifiedAccess>]
type HostProperty =
    | Name of name: string
    | Memo of memo: string
    | Metadata of metadata: HostMetadata

type HostBuilder() =
    member inline _.Yield(()) = ()

    member inline _.Run(props: HostProperty list) =
        props
        |> List.fold
            (fun host prop ->
                match prop with
                | HostProperty.Name name -> { host with Name = name }
                | HostProperty.Memo memo -> { host with Memo = Some(memo) }
                | HostProperty.Metadata metadata -> { host with Metadata = metadata :: host.Metadata })
            { Name = ""; Memo = None; Metadata = [] }

    [<CustomOperation("name")>]
    member inline _.Name((), name: string) = [ HostProperty.Name name ]

    [<CustomOperation("memo")>]
    member inline _.Memo(prev: HostProperty list, memo: string) = (HostProperty.Memo memo) :: prev

    [<CustomOperation("agent")>]
    member inline _.Agent(prev: HostProperty list, name: string, revision: string, version: string) = (Agent(name, revision, version) |> HostProperty.Metadata) :: prev

    [<CustomOperation("memory")>]
    member inline _.Memory(prev: HostProperty list, memory: (string * string) list) = (Memory memory |> HostProperty.Metadata) :: prev

[<RequireQualifiedAccess>]
type MackerelProperty =
    | ApiKey of string
    | Host of Host

type MackerelBuilder() =
    member inline _.Yield(()) = ()

    member inline _.Yield(host: Host) = MackerelProperty.Host host

    member inline _.Run(props: MackerelProperty list) =
        props
        |> List.fold
            (fun mackerel prop ->
                match prop with
                | MackerelProperty.ApiKey apiKey -> { mackerel with ApiKey = apiKey }
                | MackerelProperty.Host host -> { mackerel with Hosts = host :: mackerel.Hosts })
            { ApiKey = ""; Hosts = [] }

    member inline this.Run(prop: MackerelProperty) = this.Run([ prop ])

    member inline _.Delay(f: unit -> MackerelProperty list) = f ()

    member inline _.Delay(f: unit -> MackerelProperty) = [ f () ]

    member inline _.Combine(prop: MackerelProperty, props: MackerelProperty list) = prop :: props

    member inline this.For(prop: MackerelProperty, f: unit -> MackerelProperty list) = this.Combine(prop, f ())

    [<CustomOperation("apiKey")>]
    member inline _.ApiKey((), apiKey: string) = MackerelProperty.ApiKey apiKey

ここまでできたら冒頭の通りで、使うのは簡単です。

let host = HostBuilder()
let mackerel = MackerelBuilder()

mackerel {
    apiKey ".........."

    host {
        name "foo"

        agent "mackerel-agent/0.78.3 (Revision f41be78)" "f41be78" "0.78.3"
        memory [ "total", "33554432kB"; "something", "useful" ]
    }

    host {
        name "bar"
        memo "hoge"
    }
}

さてmackerel { ... }の返り値はMackerel型になります。 今回は使い道は本題ではないので、適当に考えるだけしてみましょうか。

let deploy mackerel =
    async {
        printfn "Deplying: %A" mackerel

        use client = new MackerelClient(mackerel.ApiKey)
        return! client.AsyncCreateOrUpdate(mackerel.Hosts) // 適当
    }

mackerel { ... }
|> deploy
|> Async.RunSynchronously
|> printfn "%A"

Terraformっぽいものが作れそうな気分になってきませんか? まぁ当然そういうのは既にあるわけで、AzureのIaCとしては Farmer なんかがありますね。

今回のコードも実装を進めて一通り使えるようにしたいので、気長にやっていこうと思います。

はてなエンジニア Advent Calendar 2023 、明日は id:mangano-ito さんです!

更に2023年のOssan.fmを総まとめ

良いお年をと言ったな、あれは嘘だ。ということで後編です。

前回でいい感じにデータが貯まったので、もうちょい遊んでみようと思ったのであった。

yohfee.hatenadiary.org

ちょうど.NET InteractiveをF#で遊んでみたかったんだよな。

前回ので形態素解析済みのデータはあるけど、.NETでもKuromojiでできるとのことなんでついでに。 Plotly.NETではいろんなチャートを出せるようだけど、結局今回はテーブルしか使わなかった。

#r "nuget: Lucene.Net.Analysis.Kuromoji, 4.8.0-beta00016"
#r "nuget: Plotly.NET"
#r "nuget: Plotly.NET.Interactive"

open System.IO
open Lucene.Net.Analysis
open Lucene.Net.Analysis.Ja
open Lucene.Net.Analysis.Ja.Dict
open Lucene.Net.Analysis.Ja.TokenAttributes
open Lucene.Net.Analysis.TokenAttributes
open Plotly.NET
open Plotly.NET.StyleParam

形態素解析した結果を使いそうなとこだけ雑に型にして、適当に品詞を判定するプロパティなどを生やしておく。 最初はモジュール関数にしようと思ったけど、F# 8でScalaっぽくアンダースコアを使って書けるようになったのをやりたかった。

type Token =
    { // 語
      term: string
      // 品詞など
      partOfSpeech: string array
      // 原形
      baseForm: string option }

    member private this.partOf part =
        Array.exists ((=) part) this.partOfSpeech

    member this.isSuffix = this.partOf "接尾"
    member this.isNonSelfReliance = this.partOf "非自立"
    member this.isPronoun = this.partOf "代名詞"

    // 有意でなさそうなものを除きたい
    member this.isSignificant =
        not this.isSuffix && not this.isNonSelfReliance && not this.isPronoun

    member this.isNoun = this.partOf "名詞" && this.isSignificant
    member this.isProperNoun = this.partOf "固有名詞" && this.isSignificant
    member this.isCustomNoun = this.partOf "カスタム名詞" && this.isSignificant
    member this.isPerson = this.partOf "人名" && this.isSignificant
    member this.isArea = this.partOf "地域" && this.isSignificant
    member this.isAdjective = this.partOf "形容詞" && not this.isNonSelfReliance

    // 動詞や形容詞などは原形が欲しいときもある
    member this.baseFormOrTerm = Option.defaultValue this.term this.baseForm

カスタム辞書と、データをロードする関数。 まぁドキュメントから察してこんな感じで。

let createDic dic =
    use stream = new StreamReader(path = dic)
    UserDictionary(stream)

let loadData dir dic =
    Directory.GetFiles(dir)
    |> Array.collect (fun file ->
        use reader = new StreamReader(file)

        use tokenizer =
            new JapaneseTokenizer(reader, dic, true, JapaneseTokenizerMode.NORMAL)

        let tsc = TokenStreamComponents(tokenizer)
        use ts = tsc.TokenStream
        ts.Reset()

        seq {
            while ts.IncrementToken() do
                let term = ts.GetAttribute<ICharTermAttribute>()
                let partOfSpeech = ts.AddAttribute<IPartOfSpeechAttribute>()
                let baseForm = ts.AddAttribute<IBaseFormAttribute>()

                yield
                    { term = term.ToString()
                      partOfSpeech = partOfSpeech.GetPartOfSpeech().Split("-")
                      baseForm = baseForm.GetBaseForm() |> Option.ofObj }
        }
        |> Array.ofSeq)

カスタム辞書はお試し用として一件だけ。

おっさんFM,おっさんFM,オッサンエフエム,名詞-カスタム名詞

今回は語の使用回数を表にしてどんな一年だったか振り返りましょうか。

let renderRanking values =
  Chart.Table(
      headerValues = [ "Rank"; "Term"; "Count" ],
      cellsValues = values,
      CellsMultiAlign = [ StyleParam.HorizontalAlign.Center; StyleParam.HorizontalAlign.Left; StyleParam.HorizontalAlign.Right ]
  )

そんじゃロードして。イクゾー!

let data = createDic "dict.csv" |> loadData "data"

(注1:今回も前回同様に解析の精度はお察しなので、あくまでネタとしてお楽しみください。)

(注2:.NET Interactiveの出力では結果を掲載し辛かったので、同じ内容をFsSpectreでコンソールに出力した結果を貼ります。)

では今年のエピソードを通して最も登場した名詞トップ50は・・・

data
|> Array.filter _.isNoun
|> Array.countBy id
|> Array.sortByDescending snd
|> Array.take 50
|> Array.mapi (fun i (t, c) -> [ (i + 1).ToString(); t.term; c.ToString() ])
|> Array.rev
|> renderRanking
┌──────┬────────────────┬───────┐
│ Rank │ Term           │ Count │
├──────┼────────────────┼───────┤
│ 50   │ 一番           │ 167   │
│ 49   │ 4              │ 172   │
│ 48   │ 動画           │ 175   │
│ 47   │ 紹介           │ 176   │
│ 46   │ 二             │ 177   │
│ 45   │ 10             │ 180   │
│ 44   │ 割             │ 184   │
│ 43   │ 辺             │ 186   │
│ 42   │ めちゃめちゃ   │ 190   │
│ 41   │ 逆             │ 190   │
│ 40   │ 5              │ 191   │
│ 39   │ 昔             │ 193   │
│ 38   │ ゲスト         │ 196   │
│ 37   │ 店             │ 202   │
│ 36   │ 家             │ 207   │
│ 35   │ 普通           │ 215   │
│ 34   │ 気持ち         │ 216   │
│ 33   │ 一緒           │ 222   │
│ 32   │ ギター         │ 226   │
│ 31   │ 子供           │ 241   │
│ 30   │ 月             │ 241   │
│ 29   │ 何             │ 251   │
│ 28   │ お願い         │ 253   │
│ 27   │ ポッドキャスト │ 260   │
│ 26   │ 最初           │ 263   │
│ 25   │ 好き           │ 264   │
│ 24   │ 京都           │ 264   │
│ 23   │ 風             │ 269   │
│ 22   │ ー             │ 276   │
│ 21   │ おっさんFM     │ 281   │
│ 20   │ 仕事           │ 287   │
│ 19   │ 映画           │ 315   │
│ 18   │ あと           │ 346   │
│ 17   │ 気             │ 354   │
│ 16   │ クリス         │ 365   │
│ 15   │ 3              │ 397   │
│ 14   │ 時間           │ 410   │
│ 13   │ 最近           │ 424   │
│ 12   │ 一             │ 481   │
│ 11   │ 前             │ 528   │
│ 10   │ 長山           │ 605   │
│ 9    │ 2              │ 670   │
│ 8    │ 今             │ 717   │
│ 7    │ 1              │ 738   │
│ 6    │ 樋口           │ 742   │
│ 5    │ 確か           │ 769   │
│ 4    │ 自分           │ 775   │
│ 3    │ 話             │ 836   │
│ 2    │ 人             │ 1162  │
│ 1    │ 感じ           │ 2208  │
└──────┴────────────────┴───────┘

前回の結果からそんな気はしてたって感じ。 とはいえ傾向的にはそれっぽい結果に見えなくもない。 樋口ってそんな出てたっけ?ていうか何。。。

人名に限ると・・・

data
|> Array.filter _.isPerson
|> Array.countBy id
|> Array.sortByDescending snd
|> Array.take 20
|> Array.mapi (fun i (t, c) -> [ (i + 1).ToString(); t.term; c.ToString() ])
|> Array.rev
|> renderRanking
┌──────┬────────────┬───────┐
│ Rank │ Term       │ Count │
├──────┼────────────┼───────┤
│ 20   │ 健         │ 15    │
│ 19   │ かおり     │ 15    │
│ 18   │ かね       │ 17    │
│ 17   │ エル       │ 20    │
│ 16   │ ジャクソン │ 20    │
│ 15   │ 宮川       │ 21    │
│ 14   │ 中山       │ 30    │
│ 13   │ 近藤       │ 35    │
│ 12   │ 竹志       │ 38    │
│ 11   │ 池田       │ 38    │
│ 10   │ 二宮       │ 39    │
│ 9    │ 永山       │ 43    │
│ 8    │ 山本       │ 46    │
│ 7    │ じゅん     │ 48    │
│ 6    │ 橋本       │ 51    │
│ 5    │ 香里       │ 57    │
│ 4    │ 笑         │ 59    │
│ 3    │ ハテナ     │ 84    │
│ 2    │ クリス     │ 365   │
│ 1    │ 長山       │ 605   │
└──────┴────────────┴───────┘

長山さんが堂々の一位ですね。 この辞書だとハテナが人名になるのかー。 エルとジャクソンが存在感を放っているけどサミュエルどこ行ったwww

そして地名だと・・・

data
|> Array.filter _.isArea
|> Array.countBy id
|> Array.sortByDescending snd
|> Array.take 20
|> Array.mapi (fun i (t, c) -> [ (i + 1).ToString(); t.term; c.ToString() ])
|> Array.rev
|> renderRanking
┌──────┬──────────────┬───────┐
│ Rank │ Term         │ Count │
├──────┼──────────────┼───────┤
│ 20   │ 関西         │ 21    │
│ 19   │ 那須塩原     │ 21    │
│ 18   │ チリ         │ 22    │
│ 17   │ 博多         │ 23    │
│ 16   │ アルゼンチン │ 23    │
│ 15   │ 倉敷         │ 23    │
│ 14   │ 大阪         │ 25    │
│ 13   │ 南米         │ 25    │
│ 12   │ 九州         │ 32    │
│ 11   │ 英           │ 35    │
│ 10   │ 深井         │ 37    │
│ 9    │ 松本         │ 55    │
│ 8    │ アメリカ     │ 60    │
│ 7    │ 岡山         │ 88    │
│ 6    │ 鹿児島       │ 89    │
│ 5    │ 福岡         │ 123   │
│ 4    │ 日本         │ 129   │
│ 3    │ 東京         │ 142   │
│ 2    │ 京都         │ 264   │
│ 1    │ 樋口         │ 742   │
└──────┴──────────────┴───────┘

樋口は地名だったのか。 Wikipediaによるとあちこちにあるし、京都にもあるみたいだしなー。 って絶対そんなことないだろって思ってWisperの書き起こしを確認してみた。

(240. 相談したいことがあるんです (ゲスト:june29さん))
毎週月曜日の朝に
一家総出で大和田朝会をやる
っていうやつで
樋口 一回仕事お二人ですよね
深井 そうです
一家総出でこの1週間どうだったかと
次の1週間何やるかを
お互い書いて
それについて喋って
じゃあこの日はこうした方がいいね
みたいなちょっとスケジュールとかを
調整したりするっていうのを始めて
それによって
曜日間隔も全て失って
ただ漂う生き物にならないように
まず月曜日の朝っていうのは
意識づけようと思って
これをまず入れましたね
こういうのないと怖い気がしたので
樋口 確かに確かに
深井 ちなみに奥様は
お仕事されてる
樋口 そうですねお仕事
会社員はやってないので
2人とも無所属なんですけども
ちょいちょいなんか
樋口 無所属で
深井 無所属で受けてやったりとか
自分の好きなものを作って
発表したりみたいな感じですね

これはダメな奴でしたね。 深井お前もか。 というわけでやっぱ京都が一位! 上位以外はゲスト回の影響をモロに受けてるのが面白い。

続いて食べ物を・・・と思ったけど分類がないので人力検索になっちゃうので、余力があるときに。 とんかつでは?と思ったけど52回だったので怪しいな。 カツだけとかかつ丼とかに分散してるのもありそう。 誰か当ててみてください、コメントいただければ調べます。

気を取り直して形容詞では・・・

data
|> Array.filter _.isAdjective
|> Array.countBy _.baseFormOrTerm
|> Array.sortByDescending snd
|> Array.take 20
|> Array.mapi (fun i (t, c) -> [ (i + 1).ToString(); t; c.ToString() ])
|> Array.rev
|> renderRanking
┌──────┬──────────┬───────┐
│ Rank │ Term     │ Count │
├──────┼──────────┼───────┤
│ 20   │ 嬉しい   │ 96    │
│ 19   │ 安い     │ 98    │
│ 18   │ うまい   │ 106   │
│ 17   │ 怖い     │ 106   │
│ 16   │ 新しい   │ 108   │
│ 15   │ 大きい   │ 112   │
│ 14   │ っぽい   │ 115   │
│ 13   │ 早い     │ 116   │
│ 12   │ 悪い     │ 134   │
│ 11   │ 美味しい │ 139   │
│ 10   │ よい     │ 177   │
│ 9    │ 難しい   │ 192   │
│ 8    │ 高い     │ 201   │
│ 7    │ 多い     │ 269   │
│ 6    │ 良い     │ 280   │
│ 5    │ 楽しい   │ 320   │
│ 4    │ 面白い   │ 662   │
│ 3    │ すごい   │ 1092  │
│ 2    │ ない     │ 1240  │
│ 1    │ いい     │ 1414  │
└──────┴──────────┴───────┘

いいですねー。すごい!面白い!楽しい!良い!最高!!!!

いかがでしたか? 誰か真面目にガチでやってください。

そういえばOssan.fmパーカーも素敵ですね。

それでは地獄の年越しサーモンラン祭りでお会いしましょう。

2023年のOssan.fmを勝手にハイライトする機構

本日のレシピはこちら。 年末にやりがちなやつです。

# 作業ディレクトリを掘って
New-Item -ItemType Directory -Path "mp3", "text", "fixed", "csv", "word", "image"

# 2023年は第222回から273回まで
for ($i = 222; $i -le 273; $i++) {
  # エピソードをダウンロードして
  Invoke-WebRequest -Uri "https://cdn.ossan.fm/audio/$i.mp3" -OutFile "mp3/$i.mp3"

  # 文字起こしして
  whisper "mp3/$i.mp3" --language ja --model large --output_dir text --output_format txt

  # 最低限の修正だけして
  (Get-Content "text/$i.txt").Replace("おっさんFM", "Ossanfm").Replace("クリス", "栗栖") > "fixed/$i.txt"

  # 形態素解析して
  python -X utf8 word.py dic/ipadic-mecab-2_7_0/system.dic.zst "fixed/$i.txt" > "csv/$i.csv"

  # 何となくそれっぽい結果になる名詞だけ取り出して
  ((
    Import-Csv -Path "csv/$i.csv" -Header "surface", "pos", "pos_detail_1", "pos_detail_2", "pos_detail_3", "conjugated_type", "conjugated_form", "basic_form", "reading", "pronunciation" |
    Where-Object { $_.pos -eq "名詞" -and $_.pos_detail_1 -ne "代名詞" -and $_.pos_detail_1 -ne "非自立" -and $_.pos_detail_1 -ne "接尾" } |
    ForEach-Object { $_.surface } |
    Out-String -Stream
  ) -join " ").Replace("ハテナ", "はてな") > "word/$i.txt"
  # 「はてな」で形態素解析にかけると感動詞に分類されちゃうのでこれだけ後で直して

  # ワードクラウドにする
  wordcloud_cli --text "word/$i.txt" --imagefile "image/$i.png" --fontfile azukiP.ttf --no_collocations --max_words 100 --width 720 --height 360
}

Whisperの精度は悪くはないけど惜しいなって感じだった。 とはいえいろいろ調整どころがあるだろうから追々で。 ここで校正を頑張った方がいいんだけどWhisper先生の今後の活躍に期待する体で今回は手抜きの構え。 多少時間はかかるけどローカルで好きなだけブン回せるのは助かる。

形態素解析は単純にこんな感じ。 ここでも辞書を換えたりユーザー辞書を作ったりした方がより番組にマッチしたワードを拾えるだろうけど。

# word.py

import sys
import vibrato
import zstandard

dic = sys.argv[1]
txt = sys.argv[2]

dctx = zstandard.ZstdDecompressor()
with open(dic, "rb") as fp:
  with dctx.stream_reader(fp) as dict_reader:
    tokenizer = vibrato.Vibrato(dict_reader.read())

with open(txt, "r") as f:
  text = f.read().strip()

tokens = tokenizer.tokenize(text)

for token in tokens:
  print(f"{token.surface()},{token.feature()}")

全部載せてもアレなので印象深いエピソードをいくつか。 一応エクスキューズを置いておくと、先の通りデータの精度はお察しなのでネタとしてお楽しみください。

224. 鹿児島旅行のおすすめポイント

230. とんかつの話をしましょう(前編) (ゲスト:kudakurageさん)

231. とんかつの話をしましょう(後編) (ゲスト:kudakurageさん)

236. Do It Yourself (ゲスト:kobakenさん)

239. 松本市での暮らしぶり (ゲスト:june29さん)

243. 2ヶ月間の南米旅行(ゲスト:セコンさん)

254. お気に入りのコンビニ

260. 5周年

263. 不機嫌なおじさん

267. おじさんの趣味マップ

272. どういう顔していいか分からないとき

273. 2023年の振り返り

掲載しなかったものも含め各回それぞれ特徴があって面白いwww 「感じ」と「人」が無駄に多いから作為的に抜いてもよかったかなとも思ったけど、それもまた味っぽそうなのでそのままで。 一通り眺めて、確かにそんな話あったなーも、そんな話あったっけもあったけどまぁ良かろう。

今年も楽しい番組ありがとうございました。 そんじゃー良いお年を。

PowerShellでMackerelにホストメトリックを投稿する

この記事は Mackerel Advent Calendar 2023 の7日目です。

qiita.com

6日目は id:minemuracoffee さんでした。

www.minemura-coffee.com

本題

ホストメトリックの投稿API を利用するとMackerel Agentを介さずにホストメトリックを投稿することができます。

お馴染みのcurlコマンドだとこんな感じですね。

curl https://api.mackerelio.com/api/v0/tsdb \
  -X POST \
  -H 'X-Api-Key: MACKEREL_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '[
    {"hostId": "HOST_ID", "name": "Sample.foo", "time": '$(date +%s)', "value": 30},
    {"hostId": "HOST_ID", "name": "Sample.bar", "time": '$(date +%s)', "value": 100}
  ]'

まずは上記の例をそのままPowerShellにしてみましょう。

Invoke-WebRequest -Uri https://api.mackerelio.com/api/v0/services/SERVICE_NAME/tsdb `
  -Method Post `
  -Headers @{
    "X-Api-Key" = "MACKEREL_API_KEY"
    "Content-Type" = "application/json"
  } `
  -Body (@(
    @{ hostId = "HOST_ID"; name = "Sample.foo"; time = [int64](Get-Date -UFormat %s); value = 30 },
    @{ hostId = "HOST_ID"; name = "Sample.bar"; time = [int64](Get-Date -UFormat %s); value = 100 }
  ) | ConvertTo-Json)

リクエストボディはパイプでInvoke-WebRequestコマンドレットに渡すことができるので、このように書き換えられます。

@(
  @{ hostId = "HOST_ID"; name = "Sample.foo"; time = [int64](Get-Date -UFormat %s); value = 30 },
  @{ hostId = "HOST_ID"; name = "Sample.bar"; time = [int64](Get-Date -UFormat %s); value = 100 }
) |
ConvertTo-Json |
Invoke-WebRequest -Uri https://api.mackerelio.com/api/v0/services/SERVICE_NAME/tsdb `
  -Method Post `
  -Headers @{
    "X-Api-Key" = "MACKEREL_API_KEY"
    "Content-Type" = "application/json"
  }

つまり次のように一般化できそうです。

何かを | いじって | ConvertTo-Json | Invoke-WebRequest ...

いくつか例を挙げてみましょう。 なお今回はグラフ定義については割愛するため、必要に応じて グラフ定義の投稿API を参照して設定してください。

  • CPU使用率上位5プロセス
Get-Process |
Select-Object `
  @{Name="hostId";Expression={"HOST_ID"}},
  @{Name="name";Expression={"Process." + $_.ProcessName}},
  @{Name="value";Expression={$_.CPU}},
  @{Name="time";Expression={[int64](Get-Date -UFormat %s)}} |
Sort-Object -Property value -Bottom 5 |
ConvertTo-Json |
iwr ...
  • mackerel-agentプロセスのメモリ使用量

(...).PSObject.Properties.GetEnumerator()のところはもうちょっとなんとかしたかったがわからんかった…

(
  Get-Process mackerel-agent |
  Select-Object -Property PagedMemorySize64, PagedSystemMemorySize64, PeakPagedMemorySize64
).PSObject.Properties.GetEnumerator() |
ForEach-Object {
  @{
    hostId = "HOST_ID"
    name = "mackerel-agent.memory." + $_.Name
    value = $_.Value
    time = [int64](Get-Date -UFormat %s)
  }
} |
ConvertTo-Json |
iwr ...
  • mackerel-agentサービスの稼働(Runningなら1、それ以外なら0)

1要素だと配列にならないので先頭にカンマが要る謎仕様…

,@(@{
  hostId = "HOST_ID"
  name = "mackerel-agent.status.running"
  value = if ((Get-Service mackerel-agent).Status -eq "Running") { 1 } else { 0 }
  time = [int64](Get-Date -UFormat %s)
}) |
ConvertTo-Json |
iwr ...

こういうのだとやはりグラフ定義を利用してまとめて表示したくなりますね。

(Get-Counter "\FileSystem Disk Activity(*)\*").CounterSamples | 
Select-Object `
  @{Name="hostId";Expression={"HOST_ID"}},
  @{Name="name";Expression={"FileSystemDiskActivity." + $_.InstanceName + "." + $_.Path.Split(" ")[-1]}},
  @{Name="value";Expression={$_.CookedValue}},
  @{Name="time";Expression={[int64](Get-Date -UFormat %s)}} |
ConvertTo-Json |
iwr ...

ちなみにホストIDはMackerel Agentが稼働しているホストなら C:\Program Files\Mackerel\mackerel-agent\id にあるので、下記のように変数や環境変数に入れておくと捗ります。

$hostId = Get-Content -Path "C:\Program Files\Mackerel\mackerel-agent\id"
$Env:HOST_ID = Get-Content -Path "C:\Program Files\Mackerel\mackerel-agent\id"

あとはタスクスケジューラにでも登録するなり、Mackerel Agent Pluginとして仕上げるなりするとよさそうです。 またサービスメトリックも同じ要領で投稿できるので、Azureモジュールを利用するAzure Functionsなども考えられます。

利用可能なコマンドレットやパフォーマンスカウンター自体も取得できるので、眺めてみるといいアイディアが閃くかもしれません。

Get-Command -Verb Get
Get-Counter -ListSet *

Mackerel Advent Calendar 2023 8日目は id:ne-sachirou さんです。

[PR] Mackerel Meetup #15 Tokyoを2023年12月19日(火)に開催します

「チームとコミュニティで監視を育てる」をテーマに、監視を育てるスタート地点でもあり、考え方でもある「SRE」の概念やその導入方法、具体的な実装について知ることのできるコンテンツを用意しています。 Mackerelをお使いの方も、これから使い始めようという方も、明日から自分たちの監視やシステムを育てるヒントにしていただけたら幸いです。ぜひMackerelチームメンバーに会いに来てください!

詳細とご応募はこちらから!

mackerelio.connpass.com

Windowsターミナルで無限のCTO

ちょっと前からWindowsターミナルの背景にHLSLでシェーダーを設定できるようになっている。

{
  "profiles": {
    "defaults": {
      "experimental.pixelShaderPath": "path\\to\\some.hlsl"
    }
  }
}

みんな大好きハンゼルマン先生がいくつか公開してるので取り合えず覗いてみるといいだろう。

GitHub - Hammster/windows-terminal-shaders: A small collection of terminal shaders

個人的にはdawnstarsneonroadあたりがいい感じ。 あるいはhueshiftCHANGE_RATEをお好みで増やしてゲーミングターミナルにしたりなど。

さて、やはり自分でも何か書いてみたくなるのがプログラマ人情というもの。

でもHLSLはさっぱりで先のコードを読み解くのは初見には流石にハードなのでもっとサンプル感のあるコードを見てなるほどを得る。

terminal/samples/PixelShaders at main · microsoft/terminal · GitHub

というわけで早速いっちょ書いてみたのがこちら。

Texture2D shaderTexture;
SamplerState samplerState;

cbuffer PixelShaderSettings {
  float  Time;
  float  Scale;
  float2 Resolution;
  float4 Background;
};

static float3 motemen[16] = {
  float3(0.47, 0.51, 0.55), float3(0.99, 1.00, 0.96), float3(0.08, 0.23, 0.54), float3(0.62, 0.65, 0.73),
  float3(0.51, 0.32, 0.15), float3(1.00, 0.88, 0.83), float3(1.00, 0.88, 0.83), float3(0.85, 0.18, 0.13),
  float3(0.64, 0.67, 0.76), float3(0.99, 0.77, 0.72), float3(1.00, 0.70, 0.64), float3(0.88, 0.42, 0.36),
  float3(0.68, 0.70, 0.72), float3(0.22, 0.24, 0.27), float3(0.33, 0.13, 0.05), float3(0.35, 0.35, 0.37)
};

float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET {
  float4 color = shaderTexture.Sample(samplerState, tex);
  float4 ocolor = shaderTexture.Sample(samplerState, tex + 2.0 * Scale * float2(-1.0, -1.0) / Resolution.y);

  float3 bg = motemen[int(tex.x * 4) + 4 * int(tex.y * 4)];
  return float4(lerp(lerp(bg, float(0.0), ocolor.w), color.xyz, color.w), 1.0);
}

これを適用するとこうなる。 偶然にもCTOのアイコンがシェーダー入門者にやさしかったので助かった。*1

とはいえこんなんじゃただ画像が貼ってあるのと変わらんので、シェーダーらしさをひとつまみ。

Texture2D shaderTexture;
SamplerState samplerState;

cbuffer PixelShaderSettings {
  float  Time;
  float  Scale;
  float2 Resolution;
  float4 Background;
};

static float3 motemen[16] = {
  float3(0.47, 0.51, 0.55), float3(0.99, 1.00, 0.96), float3(0.08, 0.23, 0.54), float3(0.62, 0.65, 0.73),
  float3(0.51, 0.32, 0.15), float3(1.00, 0.88, 0.83), float3(1.00, 0.88, 0.83), float3(0.85, 0.18, 0.13),
  float3(0.64, 0.67, 0.76), float3(0.99, 0.77, 0.72), float3(1.00, 0.70, 0.64), float3(0.88, 0.42, 0.36),
  float3(0.68, 0.70, 0.72), float3(0.22, 0.24, 0.27), float3(0.33, 0.13, 0.05), float3(0.35, 0.35, 0.37)
};

float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET {
  float4 color = shaderTexture.Sample(samplerState, tex);
  float4 ocolor = shaderTexture.Sample(samplerState, tex + 2.0 * Scale * float2(-1.0, -1.0) / Resolution.y);

  tex.xy += (Time % 10) / 10;
  if (tex.x > 1) {
    tex.x -= 1;
  }
  if (tex.y > 1) {
    tex.y -= 1;
  }

  float3 bg = motemen[int(tex.x * 4) + 4 * int(tex.y * 4)];
  return float4(lerp(lerp(bg, float(0.0), ocolor.w), color.xyz, color.w), 1.0);
}

これが∞motemenさんか…

youtu.be

ところで、いろいろなシェーダーを気分で切り替えたいので、いちいち設定ファイルを開いて書き換えるのは面倒である。 ということで、これをお手軽にできるPowerShellのモジュールをF#で作った話を次回する予定。

ちなみに常時オンだとやっぱアレなので次のようにしてショートカットキーで切り替えられるようにしている。

{
  "actions": [
    {
      "command": "toggleShaderEffects",
      "keys": "shift+f10"
    }
  ]
}

ひたすら楽してMackerelエージェント@Windows

PowerShellに下記コピペ。管理者権限またはダイアログが出ていいなら管理者に昇格できるユーザで。APIキーは各自のものを。

icm -ScriptBlock ([ScriptBlock]::Create((iwr https://gist.githubusercontent.com/yohfee/ca901162d2d295174c269d8f289519d7/raw/b163b870222be3cea8dacf35263f151db89c3824/Setup.ps1).Content)) -ArgumentList "YOUR_API_KEY_HERE"

多分これが一番楽だと思います。

ちなみに中身はこんな感じで大したことはしてない。

gist.github.com

それはそれとして夏休みの自由研究としてF#でバイナリモジュール書いてるやつで色々とお手軽になると嬉しそう。