logicmonclient.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. /*
  2. This Source Code Form is subject to the terms of the Mozilla Public
  3. License, v. 2.0. If a copy of the MPL was not distributed with this
  4. file, You can obtain one at https://mozilla.org/MPL/2.0/.
  5. */
  6. package logicmonclient
  7. // https://www.logicmonitor.com/support/rest-api-developers-guide/overview/using-logicmonitors-rest-api
  8. // https://www.logicmonitor.com/swagger-ui-master/api-v3/lm-sdkv3-docs.html
  9. import (
  10. "context"
  11. "crypto/hmac"
  12. "crypto/sha256"
  13. "encoding/base64"
  14. "encoding/hex"
  15. "encoding/json"
  16. "fmt"
  17. "io"
  18. "log"
  19. "net/http"
  20. "net/url"
  21. "strconv"
  22. "time"
  23. dp "idio.link/go/depager"
  24. )
  25. var Debug = false
  26. // LogicMonitor REST API v3 supports maximum size of 1000
  27. // records. Even if you specify a number above 1000, the
  28. // response will always return 1000 records.
  29. const MaxPageSize = 1000
  30. func debug(format string, msg ...any) {
  31. if Debug {
  32. log.Printf("DEBUG: "+format, msg...)
  33. }
  34. }
  35. type LMv1TokenBasedCredential struct {
  36. AccessId string
  37. AccessKey string
  38. }
  39. func NewLMv1TokenBasedCredential(
  40. accessId,
  41. accessKey string,
  42. ) *LMv1TokenBasedCredential {
  43. return &LMv1TokenBasedCredential{accessId, accessKey}
  44. }
  45. func generateAuthorization(
  46. timestamp,
  47. httpMethod,
  48. resource string,
  49. requestBody []byte,
  50. cred *LMv1TokenBasedCredential,
  51. ) string {
  52. /* From https://www.logicmonitor.com/support/rest-api-authentication:
  53. signature =
  54. base64(HMAC-SHA256(
  55. Access Key,
  56. HTTP VERB +
  57. TIMESTAMP (in epoch milliseconds) +
  58. POST/PUT DATA (if any) +
  59. RESOURCE PATH,
  60. ))
  61. Authorization: LMv1 AccessId:Signature:Timestamp
  62. */
  63. mac := hmac.New(sha256.New, []byte(cred.AccessKey))
  64. mac.Write([]byte(httpMethod))
  65. mac.Write([]byte(timestamp))
  66. mac.Write(requestBody)
  67. mac.Write([]byte(resource))
  68. sum := hex.EncodeToString(mac.Sum(nil))
  69. sig := base64.StdEncoding.EncodeToString([]byte(sum))
  70. auth := fmt.Sprintf(
  71. "LMv1 %s:%s:%s",
  72. cred.AccessId,
  73. sig,
  74. timestamp,
  75. )
  76. debug("authorization: %v", auth)
  77. return auth
  78. }
  79. func NewLMClient(
  80. accountName string,
  81. ctx context.Context,
  82. httpClient *http.Client,
  83. credential *LMv1TokenBasedCredential,
  84. ) *LMClient {
  85. return &LMClient{
  86. accountName: accountName,
  87. domain: "logicmonitor.com",
  88. basePath: "/santaba/rest",
  89. ctx: ctx,
  90. httpClient: httpClient,
  91. credential: credential,
  92. retries: 3,
  93. retryWait: 10 * time.Second,
  94. }
  95. }
  96. type LMClient struct {
  97. accountName string
  98. domain string
  99. basePath string
  100. ctx context.Context
  101. httpClient *http.Client
  102. credential *LMv1TokenBasedCredential
  103. blockReqsBefore time.Time
  104. retries int
  105. retryWait time.Duration
  106. }
  107. func (c *LMClient) request(
  108. method string,
  109. uri *url.URL,
  110. body io.Reader,
  111. ) (respBody []byte, err error) {
  112. uri.Scheme = "https"
  113. uri.Host = c.accountName + "." + c.domain
  114. resource := uri.EscapedPath() // save for authentication
  115. path, err := url.JoinPath(c.basePath, uri.EscapedPath())
  116. if err != nil {
  117. panic(fmt.Sprintf("BUG: lmclient: request: failed to join path: %s + %s", c.basePath, uri.RequestURI()))
  118. }
  119. uri.Path = path
  120. debug("request %s", uri)
  121. req, err :=
  122. http.NewRequestWithContext(
  123. c.ctx,
  124. method,
  125. uri.String(),
  126. body,
  127. )
  128. if err != nil {
  129. err = fmt.Errorf("request %v: %v", uri, err)
  130. return
  131. }
  132. var reqBody []byte
  133. if req.Body != nil {
  134. reqBody, err = io.ReadAll(req.Body)
  135. if err != nil {
  136. err = fmt.Errorf("request %v: failed to read request body: %v", uri, err)
  137. return
  138. }
  139. }
  140. // Only support API version 3
  141. req.Header.Set("X-version", "3")
  142. req.Header.Set("User-Agent", "logicmonclient")
  143. /* From https://www.logicmonitor.com/support/rest-api-authentication:
  144. > When LogicMonitor servers receive an API request, they
  145. > ensure the specified timestamp is within 30 minutes of
  146. > the current time.
  147. In the worst case, the below logic will fail long before
  148. the timestamp disparity reaches 30 minutes.
  149. */
  150. timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
  151. auth := generateAuthorization(
  152. timestamp,
  153. req.Method,
  154. resource,
  155. reqBody,
  156. c.credential,
  157. )
  158. req.Header.Set("Authorization", auth)
  159. // retry when server cannot handle request
  160. var resp *http.Response
  161. for i := 0; i < c.retries+1; i++ {
  162. resp, err = c.httpClient.Do(req)
  163. if err != nil {
  164. err = fmt.Errorf("request %v: %v", uri, err)
  165. return
  166. }
  167. if resp.StatusCode != http.StatusTooManyRequests {
  168. break
  169. }
  170. // Offer an async option in future, but this is
  171. // acceptable, for now.
  172. time.Sleep(c.retryWait / time.Millisecond)
  173. }
  174. defer resp.Body.Close()
  175. respBody, err = io.ReadAll(resp.Body)
  176. if err != nil {
  177. err = fmt.Errorf("request %v: failed to read response body: %v", uri, err)
  178. return
  179. }
  180. // Throttle requests
  181. // See https://www.logicmonitor.com/support/rest-api-developers-guide/overview/rate-limiting
  182. remStr := resp.Header.Get("X-Rate-Limit-Remaining")
  183. winStr := resp.Header.Get("X-Rate-Limit-Window")
  184. remaining, err := strconv.ParseInt(remStr, 10, 64)
  185. if remStr != "" && err != nil {
  186. err = fmt.Errorf("request %v: failed to parse header X-Rate-Limit-Remaining: %v", uri, err)
  187. return
  188. }
  189. window, err := time.ParseDuration(winStr + "s")
  190. if winStr != "" && err != nil {
  191. err = fmt.Errorf("request %v: failed to parse header X-Rate-Limit-Window: %v", uri, err)
  192. return
  193. }
  194. if remaining != 0 && window != 0 {
  195. // We postpone subsequent requests by at least win/rem
  196. msDelay := int64(window/time.Millisecond) / remaining
  197. delay := time.Duration(msDelay) * time.Millisecond
  198. c.blockReqsBefore = time.Now().Add(delay)
  199. }
  200. // Success response
  201. if 200 <= resp.StatusCode && resp.StatusCode <= 299 {
  202. return
  203. }
  204. // Non-success HTTP status codes with error body
  205. //err, ok := httpStatusToError[resp.StatusCode]
  206. // Error JSON in LogicMonitor response
  207. lmerr := &LMError{}
  208. err = json.Unmarshal(respBody, lmerr)
  209. if err == nil {
  210. err = fmt.Errorf("request %v: %w", uri, lmerr)
  211. return
  212. }
  213. /* Non-standard HTTP status codes
  214. This may not be necessary, but I can't tell from the
  215. documentation alone, which claims they "use standard HTTP
  216. error codes to indicate the error in the response body."
  217. This may be removed at some point.
  218. */
  219. err, ok := lmStatusToError[resp.StatusCode]
  220. if ok {
  221. return
  222. }
  223. /* Non-success HTTP status codes without error body */
  224. err, ok = httpStatusToError[resp.StatusCode]
  225. if ok {
  226. return
  227. }
  228. panic(fmt.Sprintf("BUG: unexpected response: %v", string(respBody)))
  229. }
  230. func (c *LMClient) Get(resource url.URL) ([]byte, error) {
  231. return c.request(http.MethodGet, &resource, nil)
  232. }
  233. type pagedURI struct {
  234. uri *url.URL
  235. }
  236. func (u pagedURI) PageURI(limit, offset uint64) url.URL {
  237. if limit > MaxPageSize {
  238. limit = MaxPageSize
  239. }
  240. uri := *u.uri
  241. q := (&uri).Query()
  242. q.Add("offset", strconv.FormatUint(offset, 10))
  243. q.Add("size", strconv.FormatUint(limit, 10))
  244. (&uri).RawQuery = q.Encode()
  245. return uri
  246. }
  247. func NewLMAggregate[T any]() *LMAggregate[T] {
  248. return &LMAggregate[T]{Items: make([]T, 0, 64)}
  249. }
  250. type LMAggregate[T any] struct {
  251. Total int32 `json:"total"`
  252. Items []T `json:"items"`
  253. }
  254. func (a *LMAggregate[_]) Count() uint64 {
  255. return uint64(a.Total)
  256. }
  257. func (a *LMAggregate[T]) Elems() []T {
  258. return a.Items
  259. }
  260. func makeLMPager[T any](
  261. client *LMClient,
  262. resource *url.URL,
  263. ) dp.Pager[T] {
  264. return dp.NewPager(
  265. pagedURI{resource},
  266. client,
  267. MaxPageSize,
  268. func() dp.Page[T] {
  269. return NewLMAggregate[T]()
  270. },
  271. )
  272. }
  273. type LMGraphLine struct {
  274. Legend string `json:"legend"` // "192.0.2.1:0"
  275. Type string `json:"type"` // "Stack"
  276. Data []float32
  277. }
  278. type LMTopTalkersGraph struct {
  279. Type string `json:"type"` // "graph"
  280. Id int32 `json:"id"`
  281. Name string `json:"name"` // "total"
  282. Title string `json:"title"` // "Throughput"
  283. VerticalLabel string `json:"verticalLabel"`
  284. Rigid bool `json:"rigid"` // false
  285. Width int32 `json:"width"`
  286. Height int32 `json:"height"`
  287. DisplayPrio int32 `json:"displayPrio"`
  288. TimeScale string `json:"timeScale"`
  289. Base1024 bool `json:"base1024"`
  290. //MaxValue int32 `json:"maxValue"` may return NaN
  291. //MinValue int32 `json:"minValue"` may return NaN
  292. StartTime int64 `json:"startTime"`
  293. EndTime int64 `json:"endTime"`
  294. TimeZone string `json:"timeZone"` // "CDT"
  295. TimeZoneId string `json:"timeZoneId"` // America/Chicago
  296. StartTZOffset int32 `json:"startTZOffset"`
  297. EndTZOffset int32 `json:"endTZOffset"`
  298. Step int32 `json:"step"` // 0
  299. DsName string `json:"dsName"` // null
  300. XAxisName string `json:"xAxisName"` // "2023-08-08 06:30 to 2023-08-08 19:30 CDT"
  301. Base int32 `json:"base"` // 1000
  302. ExportFileName string `json:"exportFileName"` // "Adams2120-RTR-jds-2023-08-08_06:30_to_2023-08-08_19:30_CDT"
  303. Scopes []*struct { // probably don't need this
  304. Type string `json:"type"` // "device"
  305. Id int32 `json:"id"`
  306. } `json:"scopes"`
  307. // Instances []? not sure what this is
  308. Timestamps []int64 `json:"timestamps"`
  309. Lines []*LMGraphLine `json:"lines"`
  310. }
  311. type LMFlowDirection string
  312. const (
  313. LMFlowDirectionIngress LMFlowDirection = "ingress"
  314. LMFlowDirectionEgress LMFlowDirection = "egress"
  315. LMFlowDirectionBoth LMFlowDirection = "both"
  316. )
  317. type LMProtocol string
  318. const (
  319. LMProtocolGRE LMProtocol = "gre"
  320. )
  321. type LMTopCount int32
  322. const (
  323. LMTopCount10 LMTopCount = 10
  324. LMTopCount20 LMTopCount = 20
  325. LMTopCount50 LMTopCount = 50
  326. LMTopCount100 LMTopCount = 100
  327. )
  328. type LMIPVersion string
  329. const (
  330. LMIPVersion4 LMIPVersion = "ipv4"
  331. LMIPVersion6 LMIPVersion = "ipv6"
  332. LMIPVersionBoth LMIPVersion = "both"
  333. )
  334. type LMNetflowFilter struct {
  335. Direction LMFlowDirection `json:"direction"`
  336. IPVersion LMIPVersion `json:"ipVersion"`
  337. Protocol LMProtocol `json:"protocol"`
  338. Top LMTopCount `json:"top"`
  339. }
  340. func (f LMNetflowFilter) String() string {
  341. bytes, err := json.Marshal(f)
  342. if err != nil {
  343. panic(fmt.Sprintf("BUG: failed to marshal lmnetflowfilter: %v", err))
  344. }
  345. return string(bytes)
  346. }
  347. func (c *LMClient) GetTopTalkersGraph(
  348. id int32,
  349. start,
  350. end int64,
  351. netflowFilter LMNetflowFilter,
  352. ) (graph *LMTopTalkersGraph, err error) {
  353. resStr :=
  354. fmt.Sprintf("/device/devices/%d/topTalkersGraph", id)
  355. resource, err := url.Parse(resStr)
  356. if err != nil {
  357. panic(fmt.Sprintf("BUG: get top talkers graph: failed to parse uri path '%s': %+v", resStr, err))
  358. }
  359. query := resource.Query()
  360. query.Add("netflowFilter", netflowFilter.String())
  361. query.Add("time", "zoom")
  362. query.Add("start", strconv.FormatInt(start, 10))
  363. query.Add("end", strconv.FormatInt(end, 10))
  364. resource.RawQuery = query.Encode()
  365. body, err := c.Get(*resource)
  366. if err != nil {
  367. return
  368. }
  369. graph = &LMTopTalkersGraph{}
  370. err = json.Unmarshal(body, graph)
  371. if err != nil {
  372. err = fmt.Errorf("get top talkers graph: %v", err)
  373. return
  374. }
  375. return
  376. }
  377. type LMWidget struct {
  378. Id int32 `json:"id"`
  379. Type string `json:"type"`
  380. Name string `json:"name"`
  381. Description string `json:"description"`
  382. Timescale string `json:"timescale"`
  383. Interval int32 `json:"interval"`
  384. LastUpdateOn int64 `json:"lastUpdatedOn"`
  385. LastUpdateBy string `json:"lastUpdatedBy"`
  386. UserPermission string `json:"userPermission"`
  387. DashboardId int32 `json:"dashboardId"`
  388. Theme string `json:"theme"`
  389. //NetflowFilter string `json:"netflowFilter"`
  390. DeviceId int32 `json:"deviceId"`
  391. Graph string `json:"graph"`
  392. DeviceDisplayName string `json:"deviceDisplayName"`
  393. }
  394. // Get widgets
  395. func (c *LMClient) GetWidgetList() dp.Pager[*LMWidget] {
  396. resStr := "/dashboard/widgets"
  397. resource, err := url.Parse(resStr)
  398. if err != nil {
  399. panic(fmt.Sprintf("BUG: get widget list: failed to parse uri path '%s': %+v", resStr, err))
  400. }
  401. return makeLMPager[*LMWidget](c, resource)
  402. }
  403. type LMDevice struct {
  404. Id int32 `json:"id"`
  405. Name string `json:"name"`
  406. DisplayName string `json:"displayName"`
  407. PreferredCollectorId int32 `json:"preferredCollectorId"`
  408. }
  409. // Get devices by group id
  410. func (
  411. c *LMClient,
  412. ) GetImmediateDeviceList(
  413. id int32,
  414. ) dp.Pager[*LMDevice] {
  415. resStr := fmt.Sprintf("/device/groups/%d/devices", id)
  416. resource, err := url.Parse(resStr)
  417. if err != nil {
  418. panic(fmt.Sprintf("BUG: get widget list: failed to parse uri path '%s': %+v", resStr, err))
  419. }
  420. return makeLMPager[*LMDevice](c, resource)
  421. }
  422. type LMNetflowFlow struct {
  423. DataType string `json:"dataType"`
  424. FirstEpochInSec int64 `json:"firstEpochInSec"`
  425. LastEpochInSec int64 `json:"lastEpochInSec"`
  426. Usage float32 `json:"usage"`
  427. PercentUsage float32 `json:"percentUsage"`
  428. IfIn int64 `json:"ifIn"`
  429. IfOut int64 `json:"ifOut"`
  430. SrcASN int64 `json:"srcASN"`
  431. SrcAsnName string `json:"srcAsnName"`
  432. DstASN int64 `json:"dstASN"`
  433. DstAsnName string `json:"destAsnName"`
  434. SrcIP string `json:"srcIP"`
  435. DstIP string `json:"dstIP"`
  436. Protocol string `json:"protocol"`
  437. SrcPort int32 `json:"srcPort"`
  438. DstPort int32 `json:"dstPort"`
  439. SrcDNS string `json:"srcDNS"`
  440. DstDNS string `json:"dstDNS"`
  441. SrcMBytes float32 `json:"sourceMBytes"`
  442. DstMBytes float32 `json:"destinationMBytes"`
  443. }
  444. // Get Netflow flows by device id, date range, and Netflow
  445. // filter
  446. func (
  447. c *LMClient,
  448. ) GetNetflowFlowList(
  449. id int32,
  450. start,
  451. end int64,
  452. netflowFilter string,
  453. ) dp.Pager[*LMNetflowFlow] {
  454. resStr := fmt.Sprintf("/device/devices/%d/flows", id)
  455. resource, err := url.Parse(resStr)
  456. if err != nil {
  457. panic(fmt.Sprintf("BUG: get widget list: failed to parse uri path '%s': %+v", resStr, err))
  458. }
  459. query := resource.Query()
  460. query.Add("netflowFilter", netflowFilter)
  461. query.Add("start", strconv.FormatInt(start, 10))
  462. query.Add("end", strconv.FormatInt(end, 10))
  463. resource.RawQuery = query.Encode()
  464. return makeLMPager[*LMNetflowFlow](c, resource)
  465. }