monitoring.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  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. IF(
  47. AND(
  48. GTE($status, "400"),
  49. NOT(EQUALS($status, "401")),
  50. NOT(
  51. AND(
  52. EQUALS($status, "429"),
  53. OR(
  54. EQUALS($error.type, "GoUsageLimitError"),
  55. EQUALS($error.type, "FreeUsageLimitError")
  56. )
  57. )
  58. )
  59. ),
  60. 1,
  61. 0
  62. )`,
  63. })
  64. return honeycomb.getQuerySpecificationOutput({
  65. breakdowns: ["model"],
  66. calculatedFields: [failedHttpStatus],
  67. calculations: [
  68. { op: "COUNT", name: "TOTAL", filterCombination: "AND", filters },
  69. {
  70. op: "SUM",
  71. name: "FAILED",
  72. column: failedHttpStatus.name,
  73. filterCombination: "AND",
  74. filters,
  75. },
  76. ],
  77. formulas: [{ name: "ERROR", expression: "IF(GTE($TOTAL, 200), DIV($FAILED, $TOTAL), 0)" }],
  78. timeRange: 900,
  79. }).json
  80. }
  81. const providerHttpErrorsQuery = () => {
  82. const filters = [
  83. { column: "provider", op: "exists" },
  84. { column: "user_agent", op: "contains", value: "opencode" },
  85. ]
  86. const successHttpStatus = calculatedField({
  87. name: "is_success_http_status",
  88. expression: `IF(AND(GTE($status, "200"), LT($status, "400")), 1, 0)`,
  89. })
  90. const failedProviderHttpStatus = calculatedField({
  91. name: "is_failed_provider_http_status",
  92. expression: `IF(GT($llm.error.code, "400"), 1, 0)`,
  93. })
  94. return honeycomb.getQuerySpecificationOutput({
  95. breakdowns: ["provider"],
  96. calculatedFields: [successHttpStatus, failedProviderHttpStatus],
  97. calculations: [
  98. {
  99. op: "SUM",
  100. name: "SUCCESS",
  101. column: successHttpStatus.name,
  102. filterCombination: "AND",
  103. filters: [...filters, { column: "event_type", op: "=", value: "completions" }],
  104. },
  105. {
  106. op: "SUM",
  107. name: "FAILED",
  108. column: failedProviderHttpStatus.name,
  109. filterCombination: "AND",
  110. filters: [
  111. ...filters,
  112. { column: "event_type", op: "=", value: "llm.error" },
  113. { column: "llm.error.code", op: "!=", value: "404" },
  114. ],
  115. },
  116. ],
  117. formulas: [
  118. { name: "ERROR", expression: "IF(GTE(SUM($SUCCESS, $FAILED), 200), DIV($FAILED, SUM($SUCCESS, $FAILED)), 0)" },
  119. ],
  120. timeRange: 900,
  121. }).json
  122. }
  123. const modelLowTpsQuery = (product: "go" | "zen") => {
  124. const filters = [
  125. { column: "model", op: "exists" },
  126. { column: "event_type", op: "=", value: "completions" },
  127. { column: "user_agent", op: "contains", value: "opencode" },
  128. { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
  129. { column: "status", op: ">=", value: "200" },
  130. { column: "status", op: "<", value: "400" },
  131. { column: "tps.output", op: "exists" },
  132. ]
  133. return honeycomb.getQuerySpecificationOutput({
  134. breakdowns: ["model"],
  135. calculations: [
  136. { op: "COUNT", name: "TOTAL", filterCombination: "AND", filters },
  137. {
  138. op: "P50",
  139. name: "TPS",
  140. column: "tps.output",
  141. filterCombination: "AND",
  142. filters,
  143. },
  144. ],
  145. formulas: [{ name: "LOW_TPS", expression: "IF(GTE($TOTAL, 100), $TPS, 999)" }],
  146. timeRange: 1800,
  147. }).json
  148. }
  149. new honeycomb.Trigger("IncreasedModelHttpErrorsGo", {
  150. name: "Increased Model HTTP Errors [Go]",
  151. description,
  152. queryJson: modelHttpErrorsQuery("go"),
  153. alertType: "on_change",
  154. frequency: 300,
  155. thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
  156. recipients: [
  157. {
  158. id: webhookRecipient.id,
  159. notificationDetails: [
  160. {
  161. variables: [{ name: "type", value: "model_http_errors" }],
  162. },
  163. ],
  164. },
  165. ],
  166. })
  167. new honeycomb.Trigger("IncreasedModelHttpErrorsZen", {
  168. name: "Increased Model HTTP Errors [Zen]",
  169. description,
  170. queryJson: modelHttpErrorsQuery("zen"),
  171. alertType: "on_change",
  172. frequency: 300,
  173. thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
  174. recipients: [
  175. {
  176. id: webhookRecipient.id,
  177. notificationDetails: [
  178. {
  179. variables: [{ name: "type", value: "model_http_errors" }],
  180. },
  181. ],
  182. },
  183. ],
  184. })
  185. new honeycomb.Trigger("LowModelTpsGo", {
  186. name: "Low Model TPS [Go]",
  187. description,
  188. queryJson: modelLowTpsQuery("go"),
  189. alertType: "on_change",
  190. frequency: 600,
  191. thresholds: [{ op: "<=", value: 10, exceededLimit: 1 }],
  192. recipients: [
  193. {
  194. id: webhookRecipient.id,
  195. notificationDetails: [
  196. {
  197. variables: [{ name: "type", value: "model_low_tps" }],
  198. },
  199. ],
  200. },
  201. ],
  202. })
  203. new honeycomb.Trigger("LowModelTpsZen", {
  204. name: "Low Model TPS [Zen]",
  205. description,
  206. queryJson: modelLowTpsQuery("zen"),
  207. alertType: "on_change",
  208. frequency: 600,
  209. thresholds: [{ op: "<=", value: 10, exceededLimit: 1 }],
  210. recipients: [
  211. {
  212. id: webhookRecipient.id,
  213. notificationDetails: [
  214. {
  215. variables: [{ name: "type", value: "model_low_tps" }],
  216. },
  217. ],
  218. },
  219. ],
  220. })
  221. new honeycomb.Trigger("IncreasedProviderHttpErrors", {
  222. name: "Increased Provider HTTP Errors",
  223. description,
  224. queryJson: providerHttpErrorsQuery(),
  225. alertType: "on_change",
  226. frequency: 300,
  227. thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
  228. recipients: [
  229. {
  230. id: webhookRecipient.id,
  231. notificationDetails: [
  232. {
  233. variables: [{ name: "type", value: "provider_http_errors" }],
  234. },
  235. ],
  236. },
  237. ],
  238. })
  239. new honeycomb.Trigger("IncreasedFreeTierRequests", {
  240. name: "Increased Free Tier Requests",
  241. description,
  242. queryJson: honeycomb.getQuerySpecificationOutput({
  243. calculations: [{ op: "COUNT" }],
  244. filters: [
  245. { column: "event_type", op: "=", value: "completions" },
  246. { column: "user_agent", op: "contains", value: "opencode" },
  247. { column: "isFreeTier", op: "=", value: "true" },
  248. ],
  249. timeRange: 3600,
  250. }).json,
  251. alertType: "on_change",
  252. frequency: 900,
  253. thresholds: [{ op: ">=", value: 50, exceededLimit: 1 }],
  254. baselineDetails: [{ type: "percentage", offsetMinutes: 1440 }],
  255. recipients: [
  256. {
  257. id: webhookRecipient.id,
  258. notificationDetails: [
  259. {
  260. variables: [{ name: "type", value: "custom" }],
  261. },
  262. ],
  263. },
  264. ],
  265. })