Goのカスタムアプリケーション監視

時々あなたはそれを自分でしなければならないだけです。 プロプライエタリアプリケーションを管理する場合、利用できるユーティリティのスイートはありません。独自のソリューションを実装する必要がある可能性があります。 ただし、カスタムソリューションには引き続き監視が必要です。 このブログでは、LogicMonitorのTechOpsが一部の内部ツールのカスタム監視をどのように実装したかを示します。

XNUMX月に、データを保存するための独自の時系列データベース(TSDB)のリリースを発表しました。 私達 しませんでした 新しいTSDBをサポートするために必要なすべての内部ツールを発表します。 開発は優れたTSDBを提供しましたが、本番環境で実行するために必要なツールは提供しませんでした。そのため、TechOpsチームは、新しいTSDBアーキテクチャからユーザーデータをバックアップするための運用インフラストラクチャの構築に着手しました。

すべてのTSDBデータをバックアップするためのシステムが整ったら、そのシステムを監視する必要がありました。 詳細には触れませんが、TSDBバックアップシステムには、メタデータデータベース、外部ストレージ、バックアップエージェント、一元化されたバックアップスケジューラなど、監視が必要ないくつかのコンポーネントと、ネットワークスループットなどのパフォーマンスメトリックがあります。 CPU使用率、メモリ使用率、およびディスクパフォ​​ーマンス。

これらのほとんどは、LogicMonitorを使用して箱から出してすぐに監視するための簡単なものですが、特注のバックアップエージェントとスケジューラは両方とも Go、Googleの効率的でスケーラブルなオープンソースプログラミング言語であり、本質的に監視には向いていませんでした。 このブログの残りの部分では、これらのGoアプリケーションからカスタムメトリックを公開し、LogicMonitorでそれらを監視することがいかに簡単であったかについて説明します。

概要

Goを使用すると、アプリケーション内に単純なHTTPサーバーを作成することが信じられないほど簡単になります。 この機能と組み合わせて LogicMonitorのWebページデータ収集方法、コードからカスタムメトリックをすばやく公開し、そのデータを使用してインフラストラクチャを監視できるようになりました。

アプリケーションメトリクスの公開

プロセスの最も難しい部分は、コードから公開するメトリックを正確に決定することです。 システムとアプリケーションはそれぞれ異なるため、基本から始めて、Goランタイムライブラリを使用してGoランタイム自体に関するパフォーマンスメトリックを公開する例を示します。

インポート(
 「エンコーディング/ json」
 "ログ"
 「net / http」
 "ランタイム"
)

func Performance(w http.ResponseWriter、req * http.Request){
 結果:= make(map [string] float32)

 //ゴルーチンの数を取得します
 // https://golang.org/pkg/runtime/#NumGoroutine 
 numRoutines:= runtime.NumGoroutine()
 results ["GoRoutines"] = float32(numRoutines)

 //メモリ統計を取得します
 // https://golang.org/pkg/runtime/#MemStats 
 var memStats runtime.MemStats
 runtime.ReadMemStats(&memStats)

 //割り当てられ、まだ解放されていないバイト
 results ["MemAlloc"] = float32(memStats.Alloc) 

 //無料の数
 results ["MemFrees"] = float32(memStats.Frees) 
 
 //割り当てられ、まだ解放されていないバイト
 results ["MemHeapAlloc"] = float32(memStats.HeapAlloc) 
 
 //アイドルスパンのバイト
 results ["MemHeapIdle"] = float32(memStats.HeapIdle) 

 //非アイドルスパンのバイト
 results ["MemHeapInUse"] = float32(memStats.HeapInuse) 

 //割り当てられたオブジェクトの総数
 results ["MemHeapObjects"] = float32(memStats.HeapObjects) 

 //システムから取得したバイト
 results ["MemHeapSys"] = float32(memStats.HeapSys) 

 // mallocの数
 results ["MemMallocs"] = float32(memStats.Mallocs) 

 //ガベージコレクションの総数
 results ["MemNumGc"] = float32(memStats.NumGC) 

 //ガベージコレクタがプログラムを一時停止した合計時間
 results ["MemPauseTotalNs"] = float32(memStats.PauseTotalNs) 

 //システムから取得したバイト 
 結果["MemSys"] = float32(memStats.Sys)

 resp、err:= json.Marshal(results)
 if err!= nil {
  log.Printf( "エラー:キューメトリックをjsonにマーシャリングできませんでした")
  w.WriteHeader(http.StatusInternalServerError)
 場合} else {
  w.Write(resp)
 }
}


このコードブロックは、次の例に示すように、Goランタイムに関するメトリックを含むJSON文字列を返します。

{"GoRoutines":56、 "MemAlloc":6986048、 "MemFrees":950790800、 "MemHeapAlloc":6986048、 "MemHeapIdle":34209790、 "MemHeapInUse":13205504、 "MemHeapObjects":33145、 "MemHeapSys":47415296 MemMallocs ":950823940、" MemNumGc ":142465、" MemPauseTotalNs ":40569120000、" MemSys ":52869370}


The metrics you’re able to expose are only limited by your imagination. You’re able to write whatever code you need to grab a given metric from your application, convert it to JSON, and respond. Here’s a basic outline:

import (
 "encoding/json"
 "log"
 "net/http"
)

func FooMetric(w http.ResponseWriter, req *http.Request) {
 // Calculate or retrieve your datapoint here
 results := getFooMetric()

 resp, err := json.Marshal(results)
 if err != nil {
  log.Printf("error: couldn't marshal queue metrics to json")
  w.WriteHeader(http.StatusInternalServerError)
 } else {
  w.Write(resp)
 }
}


Serving Application Metrics

Once you’ve assembled a few functions to compile the metrics you need, it’s trivial to spin up a simple HTTP server and expose the metrics from your app:

import (
 "log"
 "net/http"
)

func StartServer() error {
 // https://golang.org/pkg/net/http 
 // Create a multiplexer to handle request routing
 h := http.NewServeMux()

 // Add resource handlers to route requests
 // The first argument, 'pattern', is used to match a request path 
 // and forward that request to the function specified by the
 // second argument, 'handler'
 // https://golang.org/pkg/net/http/#HandleFunc 
 //
 // In this case, we're mapping the URL 
 // https://localhost:8080/stats/performance 
 // to the function we created above name Performance and mapping
 // https://localhost:8080/stats/foo to the function FooMetric 
 h.HandleFunc("/stats/performance", Performance)
 h.HandleFunc("/stats/foo", FooMetric)

 // Create the HTTP server, passing in desired listener port, 
 // our multiplexer, and some timeout configurations
 // https://golang.org/pkg/net/http/#Server 
 srv := &http.Server{
  Addr: 8080,
  Handler: h,
  ReadTimeout: 10 * time.Second,
  WriteTimeout: 10 * time.Second,
 }


 // Start the HTTP server
 // https://golang.org/pkg/net/http/#Server.ListenAndServe 
 log.Printf("info: Stats server started on localhost" + statsPort)
 log.Fatal(srv.ListenAndServe())
 return nil
}


Bonus

Go also makes it extremely easy to output a real-time stack trace (which isn’t particularly applicable for using an application with LogicMonitor, but too useful not to share), as exemplified in the following:

import (
 "net/http"
 "runtime"
)

func Stacktrace(
 w http.ResponseWriter,
 req *http.Request,
) {
 buf := make([]byte, 1<<16)
 runtime.Stack(buf, true)
 w.Write(buf)
}


That’s it! Now you can serve this function from your HTTP server and view a stacktrace in your browser. Just add  h.HandleFunc(“/stacktrace”, Stacktrace) to your HTTP server handlers.

Putting It All Together

In order to facilitate inserting our code into your existing application, I’ll put everything together in a struct and then demonstrate how to include this HTTP server in your application’s startup.

Here’s our completed struct:

package app

 import (
  "encoding/json"
  "log"
  "net/http"
  "runtime"
 )

 const statsPort = ":8080"

 type StatsServer struct {}

 func s *StatsServer) Performance(
  w http.ResponseWriter, 
  req *http.Request,
 ) {
  results := make(map[string]float32)

  // get number of Goroutines
  // https://golang.org/pkg/runtime/#NumGoroutine 
  numRoutines := runtime.NumGoroutine()
  results["GoRoutines"] = float32(numRoutines)

  // get memory stats
  // https://golang.org/pkg/runtime/#MemStats 
  var memStats runtime.MemStats
  runtime.ReadMemStats(&memStats)

  // bytes allocated and not yet freed
  results["MemAlloc"] = float32(memStats.Alloc) 

  // number of frees
  results["MemFrees"] = float32(memStats.Frees) 
 
  // bytes allocated and not yet freed
  results["MemHeapAlloc"] = float32(memStats.HeapAlloc) 
 
  // bytes in idle spans
  results["MemHeapIdle"] = float32(memStats.HeapIdle) 

  // bytes in non-idle span
  results["MemHeapInUse"] = float32(memStats.HeapInuse) 

  // total number of allocated objects
  results["MemHeapObjects"] = float32(memStats.HeapObjects) 

  // bytes obtained from system
  results["MemHeapSys"] = float32(memStats.HeapSys) 

  // number of mallocs
  results["MemMallocs"] = float32(memStats.Mallocs) 
  // total number of garbage collections
  results["MemNumGc"] = float32(memStats.NumGC) 

  //total time that the garbage collector has paused the program
  results["MemPauseTotalNs"] = float32(memStats.PauseTotalNs) 

  // bytes obtained from system
  results["MemSys"] = float32(memStats.Sys) 

  resp, err := json.Marshal(results)
  if err != nil {
   log.Printf("error: couldn't marshal queue metrics to json")
   w.WriteHeader(http.StatusInternalServerError)
  } else {
   w.Write(resp)
  }
 }

 func (s *StatsServer) Stacktrace(
 w http.ResponseWriter,
 req *http.Request,
 ) {
  buf := make([]byte, 1<<16)
  runtime.Stack(buf, true)

  w.Write(buf)
 }

 func s *StatsServer) StartServer() error {
  // https://golang.org/pkg/net/http 

  // Create a multiplexer to handle request routing
  h := http.NewServeMux()

  // Add resource handlers to route requests
  // The first argument, 'pattern', is used to match a request path 
  // and forward that request to the function specified by the
  // second argument, 'handler'
  // https://golang.org/pkg/net/http/#HandleFunc 
  //
  // In this case, we're mapping the URL 
  // https://localhost:8080/stats/performance 
  // to the function we created above name Performance and mapping
  // https://localhost:8080/stats/foo to the function FooMetric 
  h.HandleFunc("/stats/performance", Performance)
  h.HandleFunc("/stats/foo", FooMetric)

  // Create the HTTP server, passing in desired listener port, 
  // our multiplexer, and some timeout configurations
  // https://golang.org/pkg/net/http/#Server 
  srv := &http.Server{
   Addr: 8080,
   Handler: h,
   ReadTimeout: 10 * time.Second,
   WriteTimeout: 10 * time.Second,
  }

  // Start the HTTP server
  // https://golang.org/pkg/net/http/#Server.ListenAndServe 
  log.Printf("info: Stats server started on localhost" + statsPort)
  log.Fatal(srv.ListenAndServe())
  return nil
 }


Now, including the HTTP server in our app is as simple as adding the lines below to the application’s startup function.

// initialize monitoring metric server
// NOTE: We must start the server as a go function or ListenAndServe 
// will block further code execution
go func() {
 stats := StatsServer{}
 stats.StartServer()
}()


You can now view your metrics by sending HTTP requests to the application.

> curl https://localhost:8080/stats/performance | jq .
{
 "GoRoutines": 56,
 "MemAlloc": 6986048,
 "MemFrees": 950790800,
 "MemHeapAlloc": 6986048,
 "MemHeapIdle": 34209790,
 "MemHeapInUse": 13205504,
 "MemHeapObjects": 33145,
 "MemHeapSys": 47415296,
 "MemMallocs": 950823940,
 "MemNumGc": 142465,
 "MemPauseTotalNs": 40569120000,
 "MemSys": 52869370
}


Now that your app is serving data, it’s time to start monitoring.

Bringing Everything Together Inside LogicMonitor

Consuming your exposed metrics is incredibly simple using a Webpage Datasource.

For example, here’s how we configured the datasource to monitor the performance of our TSDB backup scheduler:

blog_1

The value system.hostname in the Applies-To is configured to match any and all of the servers where you may be running this application.

blog_2

Notice that we’re using the resource endpoint configured in our Go HTTP server.

blog_3

For the final step, here’s an example of adding a JSON datapoint to your datasource:

blog_4

Make sure to update the JSON Path field to match the path to a given datapoint within the JSON response.

That’s it! LogicMonitor is now collecting data from the application. Metrics can be viewed by navigating to the monitored device and locating the datasource.

blog_5

Notice the datapoints corresponding to the runtime metrics exposed in the Go code.

blog_6

Now we can be confident that if, for any reason, backups of customer data are not completing in a timely manner, our Operations team will know about it in a timely manner. After all, even if you are not instrumenting your custom applications, there are still two monitoring systems that will tell you about issues – your customers and your boss. We want to make sure that we know of, and can address, any issue before those two systems trigger.