monitoring.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import { SECRET } from "./secret"
  2. import { domain } from "./stage"
  3. const description = "Managed by SST (Don't edit in Honeycomb UI)"
  4. const webhookRecipient = new honeycomb.WebhookRecipient("DiscordAlerts", {
  5. name: $app.stage === "production" ? "Discord Alerts" : `Discord Alerts (${$app.stage})`,
  6. url: `https://${domain}/honeycomb/webhook`,
  7. secret: SECRET.HoneycombWebhookSecret.result,
  8. templates: [
  9. {
  10. type: "trigger",
  11. body: `{
  12. "url": {{ .Result.URL | quote }},
  13. "type": {{ .Vars.type | quote }},
  14. "name": {{ .Name | quote }},
  15. "status": {{ .Alert.Status | quote }},
  16. "isTest": {{ .Alert.IsTest }},
  17. "groups": {{ .Result.GroupsTriggered | toJson }}
  18. }`,
  19. },
  20. ],
  21. variables: [
  22. {
  23. name: "type",
  24. },
  25. ],
  26. })
  27. // Honeycomb can keep stale query-local calculated fields when the name is unchanged,
  28. // so tie the field name to the expression while avoiding deploy-to-deploy churn.
  29. // https://github.com/honeycombio/terraform-provider-honeycombio/issues/852
  30. const calculatedField = (field: { name: string; expression: string }) => ({
  31. ...field,
  32. name: `${field.name}_${(
  33. Array.from(field.expression).reduce((result, char) => Math.imul(31, result) + char.charCodeAt(0), 0) >>> 0
  34. ).toString(36)}`,
  35. })
  36. const modelHttpErrorsQuery = (product: "go" | "zen") => {
  37. const filters = [
  38. { column: "model", op: "exists" },
  39. { column: "event_type", op: "=", value: "completions" },
  40. { column: "user_agent", op: "contains", value: "opencode" },
  41. { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
  42. ]
  43. const failedHttpStatus = calculatedField({
  44. name: "is_failed_http_status",
  45. expression:
  46. product === "go"
  47. ? `IF(AND(GTE($status, "400"), NOT(EQUALS($status, "401")), NOT(EQUALS($status, "429"))), 1, 0)`
  48. : `IF(AND(EQUALS($status, "429"), $isFreeTier), 0, AND(GTE($status, "400"), NOT(EQUALS($status, "401"))), 1, 0)`,
  49. })
  50. return honeycomb.getQuerySpecificationOutput({
  51. breakdowns: ["model"],
  52. calculatedFields: [failedHttpStatus],
  53. calculations: [
  54. { op: "COUNT", name: "TOTAL", filterCombination: "AND", filters },
  55. {
  56. op: "SUM",
  57. name: "FAILED",
  58. column: failedHttpStatus.name,
  59. filterCombination: "AND",
  60. filters,
  61. },
  62. ],
  63. formulas: [{ name: "ERROR", expression: "IF(GTE($TOTAL, 100), DIV($FAILED, $TOTAL), 0)" }],
  64. timeRange: 900,
  65. }).json
  66. }
  67. const providerHttpErrorsQuery = () => {
  68. const filters = [
  69. { column: "provider", op: "exists" },
  70. { column: "user_agent", op: "contains", value: "opencode" },
  71. ]
  72. const successHttpStatus = calculatedField({
  73. name: "is_success_http_status",
  74. expression: `IF(AND(GTE($status, "200"), LT($status, "400")), 1, 0)`,
  75. })
  76. const failedProviderHttpStatus = calculatedField({
  77. name: "is_failed_provider_http_status",
  78. expression: `IF(GT($llm.error.code, "400"), 1, 0)`,
  79. })
  80. return honeycomb.getQuerySpecificationOutput({
  81. breakdowns: ["provider"],
  82. calculatedFields: [successHttpStatus, failedProviderHttpStatus],
  83. calculations: [
  84. {
  85. op: "SUM",
  86. name: "SUCCESS",
  87. column: successHttpStatus.name,
  88. filterCombination: "AND",
  89. filters: [...filters, { column: "event_type", op: "=", value: "completions" }],
  90. },
  91. {
  92. op: "SUM",
  93. name: "FAILED",
  94. column: failedProviderHttpStatus.name,
  95. filterCombination: "AND",
  96. filters: [
  97. ...filters,
  98. { column: "event_type", op: "=", value: "llm.error" },
  99. { column: "llm.error.code", op: "!=", value: "404" },
  100. ],
  101. },
  102. ],
  103. formulas: [
  104. { name: "ERROR", expression: "IF(GTE(SUM($SUCCESS, $FAILED), 200), DIV($FAILED, SUM($SUCCESS, $FAILED)), 0)" },
  105. ],
  106. timeRange: 900,
  107. }).json
  108. }
  109. const modelLowTpsQuery = (product: "go" | "zen") => {
  110. const filters = [
  111. { column: "model", op: "exists" },
  112. { column: "event_type", op: "=", value: "completions" },
  113. { column: "user_agent", op: "contains", value: "opencode" },
  114. { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
  115. { column: "status", op: ">=", value: "200" },
  116. { column: "status", op: "<", value: "400" },
  117. { column: "tps.output", op: "exists" },
  118. ]
  119. return honeycomb.getQuerySpecificationOutput({
  120. breakdowns: ["model"],
  121. calculations: [
  122. { op: "COUNT", name: "TOTAL", filterCombination: "AND", filters },
  123. {
  124. op: "P50",
  125. name: "TPS",
  126. column: "tps.output",
  127. filterCombination: "AND",
  128. filters,
  129. },
  130. ],
  131. formulas: [{ name: "LOW_TPS", expression: "IF(GTE($TOTAL, 100), $TPS, 999)" }],
  132. timeRange: 1800,
  133. }).json
  134. }
  135. new honeycomb.Trigger("IncreasedModelHttpErrorsGo", {
  136. name: "Increased Model HTTP Errors [Go]",
  137. description,
  138. queryJson: modelHttpErrorsQuery("go"),
  139. alertType: "on_change",
  140. frequency: 300,
  141. thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
  142. recipients: [
  143. {
  144. id: webhookRecipient.id,
  145. notificationDetails: [
  146. {
  147. variables: [{ name: "type", value: "model_http_errors" }],
  148. },
  149. ],
  150. },
  151. ],
  152. })
  153. new honeycomb.Trigger("IncreasedModelHttpErrorsZen", {
  154. name: "Increased Model HTTP Errors [Zen]",
  155. description,
  156. queryJson: modelHttpErrorsQuery("zen"),
  157. alertType: "on_change",
  158. frequency: 300,
  159. thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
  160. recipients: [
  161. {
  162. id: webhookRecipient.id,
  163. notificationDetails: [
  164. {
  165. variables: [{ name: "type", value: "model_http_errors" }],
  166. },
  167. ],
  168. },
  169. ],
  170. })
  171. new honeycomb.Trigger("LowModelTpsGo", {
  172. name: "Low Model TPS [Go]",
  173. description,
  174. queryJson: modelLowTpsQuery("go"),
  175. alertType: "on_change",
  176. frequency: 600,
  177. thresholds: [{ op: "<=", value: 10, exceededLimit: 1 }],
  178. recipients: [
  179. {
  180. id: webhookRecipient.id,
  181. notificationDetails: [
  182. {
  183. variables: [{ name: "type", value: "model_low_tps" }],
  184. },
  185. ],
  186. },
  187. ],
  188. })
  189. new honeycomb.Trigger("LowModelTpsZen", {
  190. name: "Low Model TPS [Zen]",
  191. description,
  192. queryJson: modelLowTpsQuery("zen"),
  193. alertType: "on_change",
  194. frequency: 600,
  195. thresholds: [{ op: "<=", value: 10, exceededLimit: 1 }],
  196. recipients: [
  197. {
  198. id: webhookRecipient.id,
  199. notificationDetails: [
  200. {
  201. variables: [{ name: "type", value: "model_low_tps" }],
  202. },
  203. ],
  204. },
  205. ],
  206. })
  207. new honeycomb.Trigger("IncreasedProviderHttpErrors", {
  208. name: "Increased Provider HTTP Errors",
  209. description,
  210. queryJson: providerHttpErrorsQuery(),
  211. alertType: "on_change",
  212. frequency: 300,
  213. thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
  214. recipients: [
  215. {
  216. id: webhookRecipient.id,
  217. notificationDetails: [
  218. {
  219. variables: [{ name: "type", value: "provider_http_errors" }],
  220. },
  221. ],
  222. },
  223. ],
  224. })
  225. new honeycomb.Trigger("IncreasedFreeTierRequests", {
  226. name: "Increased Free Tier Requests",
  227. description,
  228. queryJson: honeycomb.getQuerySpecificationOutput({
  229. calculations: [{ op: "COUNT" }],
  230. filters: [
  231. { column: "event_type", op: "=", value: "completions" },
  232. { column: "user_agent", op: "contains", value: "opencode" },
  233. { column: "isFreeTier", op: "=", value: "true" },
  234. ],
  235. timeRange: 3600,
  236. }).json,
  237. alertType: "on_change",
  238. frequency: 900,
  239. thresholds: [{ op: ">=", value: 50, exceededLimit: 1 }],
  240. baselineDetails: [{ type: "percentage", offsetMinutes: 1440 }],
  241. recipients: [
  242. {
  243. id: webhookRecipient.id,
  244. notificationDetails: [
  245. {
  246. variables: [{ name: "type", value: "custom" }],
  247. },
  248. ],
  249. },
  250. ],
  251. })