howler-api 3.3.0.dev721__tar.gz → 3.3.0.dev735__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/PKG-INFO +5 -2
  2. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/socket.py +4 -0
  3. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/clue.py +15 -17
  4. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/app.py +5 -10
  5. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/collection.py +4 -0
  6. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/oauth.py +0 -2
  7. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/config.py +21 -24
  8. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/security/__init__.py +0 -8
  9. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/security/utils.py +4 -2
  10. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/auth_service.py +6 -4
  11. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/config_service.py +4 -0
  12. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/event_service.py +3 -0
  13. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/hit_service.py +18 -0
  14. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/user_service.py +3 -2
  15. howler_api-3.3.0.dev735/howler/telemetry.py +65 -0
  16. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/pyproject.toml +5 -2
  17. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/README.md +0 -0
  18. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/__init__.py +0 -0
  19. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/__init__.py +0 -0
  20. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/add_label.py +0 -0
  21. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/add_to_bundle.py +0 -0
  22. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/change_field.py +0 -0
  23. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/demote.py +0 -0
  24. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/example_plugin.py +0 -0
  25. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/prioritization.py +0 -0
  26. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/promote.py +0 -0
  27. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/remove_from_bundle.py +0 -0
  28. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/remove_label.py +0 -0
  29. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/transition.py +0 -0
  30. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/__init__.py +0 -0
  31. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/base.py +0 -0
  32. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/__init__.py +0 -0
  33. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/action.py +0 -0
  34. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/analytic.py +0 -0
  35. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/auth.py +0 -0
  36. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/configs.py +0 -0
  37. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/dossier.py +0 -0
  38. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/help.py +0 -0
  39. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/hit.py +0 -0
  40. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/notebook.py +0 -0
  41. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/overview.py +0 -0
  42. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/search.py +0 -0
  43. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/template.py +0 -0
  44. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/tool.py +0 -0
  45. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/user.py +0 -0
  46. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/utils/__init__.py +0 -0
  47. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/utils/etag.py +0 -0
  48. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/view.py +0 -0
  49. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/README.md +0 -0
  50. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/__init__.py +0 -0
  51. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/classification.py +0 -0
  52. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/classification.yml +0 -0
  53. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/exceptions.py +0 -0
  54. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/loader.py +0 -0
  55. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/logging/__init__.py +0 -0
  56. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/logging/audit.py +0 -0
  57. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/logging/format.py +0 -0
  58. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/net.py +0 -0
  59. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/net_static.py +0 -0
  60. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/random_user.py +0 -0
  61. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/swagger.py +0 -0
  62. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/config.py +0 -0
  63. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/cronjobs/__init__.py +0 -0
  64. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/cronjobs/retention.py +0 -0
  65. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/cronjobs/rules.py +0 -0
  66. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/cronjobs/view_cleanup.py +0 -0
  67. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/README.md +0 -0
  68. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/__init__.py +0 -0
  69. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/bulk.py +0 -0
  70. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/constants.py +0 -0
  71. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/exceptions.py +0 -0
  72. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/howler_store.py +0 -0
  73. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/migrations/fix_process.py +0 -0
  74. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/operations.py +0 -0
  75. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/schemas.py +0 -0
  76. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/store.py +0 -0
  77. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/support/__init__.py +0 -0
  78. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/support/build.py +0 -0
  79. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/support/schemas.py +0 -0
  80. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/types.py +0 -0
  81. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/error.py +0 -0
  82. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/README.md +0 -0
  83. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/__init__.py +0 -0
  84. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/generate_mitre.py +0 -0
  85. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/generate_sigma_rules.py +0 -0
  86. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/generate_tlds.py +0 -0
  87. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/reindex_data.py +0 -0
  88. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/wipe_databases.py +0 -0
  89. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/gunicorn_config.py +0 -0
  90. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/healthz.py +0 -0
  91. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/__init__.py +0 -0
  92. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/azure.py +0 -0
  93. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/discover.py +0 -0
  94. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/hit.py +0 -0
  95. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/search.py +0 -0
  96. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/workflow.py +0 -0
  97. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/ws.py +0 -0
  98. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/README.md +0 -0
  99. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/__init__.py +0 -0
  100. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/base.py +0 -0
  101. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/charter.txt +0 -0
  102. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/helper.py +0 -0
  103. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/howler_enum.py +0 -0
  104. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/__init__.py +0 -0
  105. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/action.py +0 -0
  106. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/analytic.py +0 -0
  107. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/assemblyline.py +0 -0
  108. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/aws.py +0 -0
  109. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/azure.py +0 -0
  110. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/cbs.py +0 -0
  111. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/clue.py +0 -0
  112. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/dossier.py +0 -0
  113. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/__init__.py +0 -0
  114. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/agent.py +0 -0
  115. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/autonomous_system.py +0 -0
  116. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/client.py +0 -0
  117. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/cloud.py +0 -0
  118. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/code_signature.py +0 -0
  119. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/container.py +0 -0
  120. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/dns.py +0 -0
  121. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/egress.py +0 -0
  122. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/elf.py +0 -0
  123. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/email.py +0 -0
  124. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/error.py +0 -0
  125. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/event.py +0 -0
  126. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/faas.py +0 -0
  127. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/file.py +0 -0
  128. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/geo.py +0 -0
  129. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/group.py +0 -0
  130. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/hash.py +0 -0
  131. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/host.py +0 -0
  132. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/http.py +0 -0
  133. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/ingress.py +0 -0
  134. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/interface.py +0 -0
  135. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/network.py +0 -0
  136. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/observer.py +0 -0
  137. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/organization.py +0 -0
  138. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/os.py +0 -0
  139. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/pe.py +0 -0
  140. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/process.py +0 -0
  141. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/registry.py +0 -0
  142. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/related.py +0 -0
  143. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/rule.py +0 -0
  144. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/server.py +0 -0
  145. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/threat.py +0 -0
  146. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/tls.py +0 -0
  147. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/url.py +0 -0
  148. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/user.py +0 -0
  149. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/user_agent.py +0 -0
  150. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/vulnerability.py +0 -0
  151. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/gcp.py +0 -0
  152. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/hit.py +0 -0
  153. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/howler_data.py +0 -0
  154. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/lead.py +0 -0
  155. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/localized_label.py +0 -0
  156. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/overview.py +0 -0
  157. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/pivot.py +0 -0
  158. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/template.py +0 -0
  159. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/user.py +0 -0
  160. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/view.py +0 -0
  161. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/random_data.py +0 -0
  162. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/randomizer.py +0 -0
  163. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/patched.py +0 -0
  164. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/plugins/__init__.py +0 -0
  165. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/plugins/config.py +0 -0
  166. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/__init__.py +0 -0
  167. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/README.md +0 -0
  168. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/__init__.py +0 -0
  169. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/counters.py +0 -0
  170. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/events.py +0 -0
  171. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/hash.py +0 -0
  172. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/lock.py +0 -0
  173. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/queues/__init__.py +0 -0
  174. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/queues/comms.py +0 -0
  175. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/queues/multi.py +0 -0
  176. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/queues/named.py +0 -0
  177. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/queues/priority.py +0 -0
  178. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/set.py +0 -0
  179. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  180. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/security/socket.py +0 -0
  181. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/__init__.py +0 -0
  182. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/action_service.py +0 -0
  183. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/analytic_service.py +0 -0
  184. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/dossier_service.py +0 -0
  185. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/jwt_service.py +0 -0
  186. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/lucene_service.py +0 -0
  187. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/notebook_service.py +0 -0
  188. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/overview_service.py +0 -0
  189. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/template_service.py +0 -0
  190. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/__init__.py +0 -0
  191. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/annotations.py +0 -0
  192. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/chunk.py +0 -0
  193. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/dict_utils.py +0 -0
  194. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/isotime.py +0 -0
  195. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/list_utils.py +0 -0
  196. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/lucene.py +0 -0
  197. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/path.py +0 -0
  198. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/socket_utils.py +0 -0
  199. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/str_utils.py +0 -0
  200. {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/uid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: howler-api
3
- Version: 3.3.0.dev721
3
+ Version: 3.3.0.dev735
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -21,10 +21,10 @@ Classifier: Programming Language :: Python :: 3.14
21
21
  Classifier: Topic :: Software Development :: Libraries
22
22
  Requires-Dist: apscheduler (==3.11.2)
23
23
  Requires-Dist: authlib (>=1.6.0,<2.0.0)
24
+ Requires-Dist: azure-monitor-opentelemetry (>=1.8.7,<2.0.0)
24
25
  Requires-Dist: bcrypt (==4.3.0)
25
26
  Requires-Dist: chardet (==5.2.0)
26
27
  Requires-Dist: chevron (==0.14.0)
27
- Requires-Dist: elastic-apm[flask] (>=6.22.0,<7.0.0)
28
28
  Requires-Dist: elasticsearch (==8.19.3)
29
29
  Requires-Dist: flasgger (>=0.9.7.1,<0.10.0.0)
30
30
  Requires-Dist: flask (==3.1.3)
@@ -33,6 +33,9 @@ Requires-Dist: gevent (==23.9.1)
33
33
  Requires-Dist: gunicorn (==23.0.0)
34
34
  Requires-Dist: luqum (>=1.0.0,<2.0.0)
35
35
  Requires-Dist: mergedeep (>=1.3.4,<2.0.0)
36
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.40.0)
37
+ Requires-Dist: opentelemetry-instrumentation-flask (==0.61b0)
38
+ Requires-Dist: opentelemetry-sdk (==1.40.0)
36
39
  Requires-Dist: packaging (<25.0)
37
40
  Requires-Dist: passlib (==1.7.4)
38
41
  Requires-Dist: prometheus-client (==0.24.1)
@@ -4,6 +4,7 @@ import os
4
4
  from typing import Any
5
5
 
6
6
  from flask import Blueprint, request
7
+ from opentelemetry import trace
7
8
 
8
9
  import howler.services.event_service as event_service
9
10
  from howler.api import ok, unauthorized
@@ -21,10 +22,12 @@ socket_api = Blueprint("socket", "socket", url_prefix="/socket/v1")
21
22
  socket_api._doc = "Endpoints concerning websocket connectivity between the client and server" # type: ignore
22
23
 
23
24
  logger = get_logger(__file__)
25
+ tracer = trace.get_tracer(__name__)
24
26
 
25
27
  hit_helper = OdmHelper(Hit)
26
28
 
27
29
 
30
+ @tracer.start_as_current_span(f"{__name__}.emit")
28
31
  @socket_api.route("/emit/<event>", methods=["POST"])
29
32
  def emit(event: str):
30
33
  """Emit an event to all listening websockets"""
@@ -46,6 +49,7 @@ def emit(event: str):
46
49
  return ok()
47
50
 
48
51
 
52
+ @tracer.start_as_current_span(f"{__name__}.connect")
49
53
  @socket_api.route("/connect", websocket=True) # type: ignore
50
54
  @websocket_auth(required_priv=["R"])
51
55
  def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
@@ -3,7 +3,6 @@ import time
3
3
  from typing import Callable, Optional
4
4
 
5
5
  import requests
6
- from elasticapm.traces import capture_span
7
6
  from flask import request
8
7
 
9
8
  from howler.api import bad_gateway, make_subapi_blueprint, ok
@@ -74,22 +73,21 @@ def proxy_to_clue(path, **kwargs):
74
73
  clue_token = get_token(auth_token)
75
74
 
76
75
  start = time.perf_counter()
77
- with capture_span("clue", span_type="http"):
78
- if request.method.lower() == "get":
79
- response = requests.get(
80
- f"{config.core.clue.url}/{path}",
81
- headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
82
- params=request.args.to_dict(),
83
- timeout=5 * 60,
84
- )
85
- else:
86
- response = requests.post(
87
- f"{config.core.clue.url}/{path}",
88
- json=request.json,
89
- headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
90
- params=request.args.to_dict(),
91
- timeout=5 * 60,
92
- )
76
+ if request.method.lower() == "get":
77
+ response = requests.get(
78
+ f"{config.core.clue.url}/{path}",
79
+ headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
80
+ params=request.args.to_dict(),
81
+ timeout=5 * 60,
82
+ )
83
+ else:
84
+ response = requests.post(
85
+ f"{config.core.clue.url}/{path}",
86
+ json=request.json,
87
+ headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
88
+ params=request.args.to_dict(),
89
+ timeout=5 * 60,
90
+ )
93
91
 
94
92
  logger.debug("Request to clue completed in %s ms", round(time.perf_counter() - start))
95
93
 
@@ -24,7 +24,6 @@ import logging
24
24
  from typing import Any, cast
25
25
 
26
26
  from authlib.integrations.flask_client import OAuth
27
- from elasticapm.contrib.flask import ElasticAPM
28
27
  from flasgger import Swagger
29
28
  from flask import Flask
30
29
  from flask.blueprints import Blueprint
@@ -62,14 +61,19 @@ from howler.config import (
62
61
  from howler.cronjobs import setup_jobs
63
62
  from howler.error import errors
64
63
  from howler.healthz import healthz
64
+ from howler.telemetry import setup_telemetry
65
65
 
66
66
  logger = get_logger(__file__)
67
67
 
68
+
68
69
  app = Flask(
69
70
  "howler-api",
70
71
  static_url_path="/api/static",
71
72
  static_folder=config.ui.static_folder,
72
73
  )
74
+ if config.core.telemetry.enabled:
75
+ setup_telemetry(app)
76
+
73
77
  # Disable strict check on trailing slashes for endpoints
74
78
  app.url_map.strict_slashes = False
75
79
  app.config["JSON_SORT_KEYS"] = False
@@ -204,15 +208,6 @@ if logger.parent:
204
208
  for ph in logger.parent.handlers:
205
209
  app.logger.addHandler(ph)
206
210
 
207
- # Setup APMs
208
- if config.core.metrics.apm_server.server_url is not None:
209
- logger.info("Exporting application metrics to: %s", config.core.metrics.apm_server.server_url)
210
- ElasticAPM(
211
- app,
212
- server_url=config.core.metrics.apm_server.server_url,
213
- service_name="howler_api",
214
- )
215
-
216
211
  wlog = logging.getLogger("werkzeug")
217
212
  wlog.setLevel(logging.WARNING)
218
213
  if logger.parent: # pragma: no cover
@@ -16,6 +16,7 @@ from typing import Any, Dict, Generic, Literal, Optional, TypeVar, Union, overlo
16
16
  import elasticsearch
17
17
  from datemath import dm
18
18
  from datemath.helpers import DateMathException
19
+ from opentelemetry import trace
19
20
 
20
21
  from howler import odm
21
22
  from howler.common.exceptions import HowlerRuntimeError, HowlerValueError, NonRecoverableError
@@ -66,6 +67,8 @@ console.setLevel(logging.INFO)
66
67
  console.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
67
68
  logger.addHandler(console)
68
69
 
70
+ tracer = trace.get_tracer(__name__)
71
+
69
72
  ModelType = TypeVar("ModelType", bound=Model)
70
73
 
71
74
 
@@ -524,6 +527,7 @@ class ESCollection(Generic[ModelType]):
524
527
  else:
525
528
  updated += res["updated"]
526
529
 
530
+ @tracer.start_as_current_span(f"{__name__}.commit")
527
531
  def commit(self):
528
532
  """This function should be overloaded to perform a commit of the index data of all the different hosts
529
533
  specified in self.datastore.hosts.
@@ -5,7 +5,6 @@ from typing import Any, Optional
5
5
 
6
6
  import requests
7
7
  from authlib.integrations.flask_client import OAuth
8
- from elasticapm.traces import capture_span
9
8
 
10
9
  from howler.common.exceptions import HowlerException, HowlerValueError
11
10
  from howler.common.loader import USER_TYPES
@@ -30,7 +29,6 @@ def reorder_name(name: Optional[str]) -> Optional[str]:
30
29
  return " ".join(name.split(", ", 1)[::-1])
31
30
 
32
31
 
33
- @capture_span(span_type="authentication")
34
32
  def parse_profile(profile: dict[str, Any], provider_config: OAuthProvider) -> dict[str, Any]: # noqa: C901
35
33
  """Parse a raw profile dict into a useful user data dict"""
36
34
  # Find email address and normalize it for further processing
@@ -305,27 +305,6 @@ class Auth(BaseModel):
305
305
  oauth: OAuth = OAuth()
306
306
 
307
307
 
308
- class APMServer(BaseModel):
309
- """Application Performance Monitoring (APM) server configuration.
310
-
311
- Defines the connection details for an external APM server used to
312
- collect and analyze application performance metrics.
313
- """
314
-
315
- server_url: Optional[str] = Field(default=None, description="URL to API server")
316
- token: Optional[str] = Field(default=None, description="Authentication token for server")
317
-
318
-
319
- class Metrics(BaseModel):
320
- """Metrics collection configuration.
321
-
322
- Configures how Howler collects and exports application metrics,
323
- including integration with external APM servers.
324
- """
325
-
326
- apm_server: APMServer = APMServer()
327
-
328
-
329
308
  class Retention(BaseModel):
330
309
  """Hit retention policy configuration.
331
310
 
@@ -459,18 +438,36 @@ class Notebook(BaseModel):
459
438
  )
460
439
 
461
440
 
441
+ class Telemetry(BaseModel):
442
+ """Telemetry configuration for Howler.
443
+
444
+ Controls whether tracing is enabled and which backend to use.
445
+ When using ``opentelemetry``, the OTLP exporter is configured via
446
+ standard OTEL environment variables such as OTEL_EXPORTER_OTLP_ENDPOINT
447
+ and OTEL_EXPORTER_OTLP_HEADERS.
448
+ When using ``azure_monitor``, the Azure Monitor exporter is used instead,
449
+ configured via the APPLICATIONINSIGHTS_CONNECTION_STRING environment variable.
450
+ """
451
+
452
+ enabled: bool = Field(default=False, description="Enable telemetry tracing?")
453
+ backend: str = Field(
454
+ default="opentelemetry",
455
+ description="Telemetry backend to use (e.g. 'opentelemetry', 'azure_monitor')",
456
+ )
457
+
458
+
462
459
  class Core(BaseModel):
463
460
  """Core application configuration for Howler.
464
461
 
465
- Aggregates all core service configurations including Redis, metrics,
462
+ Aggregates all core service configurations including Redis, telemetry,
466
463
  and external integrations like Clue and nbgallery notebooks.
467
464
  Also manages the loading of external plugins.
468
465
  """
469
466
 
470
467
  plugins: set[str] = Field(description="A list of external plugins to load", default=set())
471
468
 
472
- metrics: Metrics = Metrics()
473
- "Configuration for Metrics Collection"
469
+ telemetry: Telemetry = Telemetry()
470
+ "Configuration for OpenTelemetry"
474
471
 
475
472
  redis: Redis = Redis()
476
473
  "Configuration for Redis instances"
@@ -3,7 +3,6 @@ import sys
3
3
  from typing import Optional
4
4
 
5
5
  import requests
6
- from elasticapm.traces import set_user_context
7
6
  from flask import request
8
7
  from flask import session as flsk_session
9
8
  from jwt import ExpiredSignatureError
@@ -214,13 +213,6 @@ class api_login(object): # noqa: D101, N801
214
213
  FAILED_ATTEMPTS.labels("500").inc()
215
214
  return internal_error(err=e.message)
216
215
 
217
- if config.core.metrics.apm_server.server_url is not None:
218
- set_user_context(
219
- username=user.get("name", None),
220
- email=user.get("email", None),
221
- user_id=user.get("uname", None),
222
- )
223
-
224
216
  if request.path.startswith("/api/v1/clue"):
225
217
  logger.debug("Bypassing quota limits for clue enrichment")
226
218
  elif self.enforce_quota:
@@ -4,11 +4,13 @@ import re
4
4
  from typing import List, Optional
5
5
  from urllib.parse import urlparse
6
6
 
7
- from elasticapm.traces import capture_span
7
+ from opentelemetry import trace
8
8
  from passlib.hash import bcrypt
9
9
 
10
10
  from howler.config import config
11
11
 
12
+ tracer = trace.get_tracer(__name__)
13
+
12
14
  UPPERCASE = r"[A-Z]"
13
15
  LOWERCASE = r"[a-z]"
14
16
  NUMBER = r"[0-9]"
@@ -48,7 +50,7 @@ def get_password_hash(password: Optional[str]) -> Optional[str]:
48
50
  return bcrypt.hash(password)
49
51
 
50
52
 
51
- @capture_span(span_type="authentication")
53
+ @tracer.start_as_current_span("verify_password")
52
54
  def verify_password(password: str, pw_hash: str):
53
55
  """Use bcrypt to verify a user's password against the hash"""
54
56
  try:
@@ -4,8 +4,8 @@ import hmac
4
4
  from datetime import datetime
5
5
  from typing import Optional, Union
6
6
 
7
- from elasticapm.traces import capture_span
8
7
  from flask import request
8
+ from opentelemetry import trace
9
9
 
10
10
  import howler.services.jwt_service as jwt_service
11
11
  import howler.services.user_service as user_service
@@ -25,6 +25,7 @@ from howler.remote.datatypes.set import ExpiringSet
25
25
  from howler.security.utils import generate_random_secret, verify_password
26
26
 
27
27
  logger = get_logger(__file__)
28
+ tracer = trace.get_tracer(__name__)
28
29
 
29
30
  nonpersistent_config: dict[str, Union[str, int]] = {
30
31
  "host": config.core.redis.nonpersistent.host,
@@ -213,7 +214,7 @@ def validate_token(username: str, token: str) -> Optional[list[str]]:
213
214
  return None
214
215
 
215
216
 
216
- @capture_span(span_type="authentication")
217
+ @tracer.start_as_current_span("bearer_auth")
217
218
  def bearer_auth(
218
219
  data: str, skip_jwt: bool = False, skip_internal: bool = False
219
220
  ) -> tuple[Optional[User], Optional[list[str]]]:
@@ -257,7 +258,7 @@ def bearer_auth(
257
258
  raise InvalidDataException("Not a valid authentication type for this endpoint.")
258
259
 
259
260
 
260
- @capture_span(span_type="authentication")
261
+ @tracer.start_as_current_span("validate_apikey")
261
262
  def validate_apikey( # noqa: C901
262
263
  username: str, apikey: str, impersonator: Optional[User] = None
263
264
  ) -> tuple[Optional[User], Optional[list[str]]]:
@@ -321,6 +322,7 @@ def validate_apikey( # noqa: C901
321
322
  raise AccessDeniedException("API Key authentication disabled")
322
323
 
323
324
 
325
+ @tracer.start_as_current_span("validate_userpass")
324
326
  def validate_userpass(username: str, password: str) -> tuple[Optional[User], Optional[list[str]]]:
325
327
  """This function identifies the user via the user/pass functionality
326
328
 
@@ -363,7 +365,7 @@ def decode_b64(b64_str: str) -> str:
363
365
  raise InvalidDataException("Basic authentication data must be base64 encoded") from e
364
366
 
365
367
 
366
- @capture_span(span_type="authentication")
368
+ @tracer.start_as_current_span("basic_auth")
367
369
  def basic_auth(
368
370
  data: str, is_base64: bool = True, skip_apikey: bool = False, skip_password: bool = False
369
371
  ) -> tuple[User | None, list[str] | None]:
@@ -3,6 +3,7 @@ from math import ceil
3
3
  from typing import Optional
4
4
 
5
5
  from flask import request
6
+ from opentelemetry import trace
6
7
 
7
8
  import howler.services.hit_service as hit_service
8
9
  from howler.common.exceptions import ForbiddenException, HowlerException
@@ -19,6 +20,8 @@ from howler.utils.str_utils import default_string_value
19
20
 
20
21
  classification_definition = CLASSIFICATION.get_parsed_classification_definition()
21
22
 
23
+ tracer = trace.get_tracer(__name__)
24
+
22
25
  lookups = get_lookups()
23
26
 
24
27
  logger = get_logger()
@@ -61,6 +64,7 @@ def _get_apikey_max_duration():
61
64
  return amount, unit
62
65
 
63
66
 
67
+ @tracer.start_as_current_span(f"{__name__}.get_configuration")
64
68
  def get_configuration(user: User | None, **kwargs):
65
69
  """Get system configration data for the Howler API
66
70
 
@@ -2,18 +2,21 @@ import os
2
2
  from typing import Any, Callable
3
3
 
4
4
  import requests
5
+ from opentelemetry import trace
5
6
  from requests.auth import HTTPBasicAuth
6
7
 
7
8
  from howler.common.logging import get_logger
8
9
  from howler.config import DEBUG, HWL_USE_WEBSOCKET_API, config
9
10
 
10
11
  logger = get_logger(__file__)
12
+ tracer = trace.get_tracer(__name__)
11
13
 
12
14
  handlers: dict[str, list[Callable]] = {}
13
15
 
14
16
  HWL_INTERPOD_COMMS_SECRET = os.getenv("HWL_INTERPOD_COMMS_SECRET", "secret")
15
17
 
16
18
 
19
+ @tracer.start_as_current_span(f"{__name__}.emit")
17
20
  def emit(event: str, data: Any):
18
21
  """Emit a new instance of the specified event, with additional data related to that event
19
22
 
@@ -5,6 +5,7 @@ import typing
5
5
  from hashlib import sha256
6
6
  from typing import Any, Literal, Optional, Union, cast, overload
7
7
 
8
+ from opentelemetry import trace
8
9
  from prometheus_client import Counter
9
10
 
10
11
  import howler.services.event_service as event_service
@@ -36,9 +37,11 @@ from howler.utils.uid import get_random_id
36
37
 
37
38
  logger = get_logger(__file__)
38
39
 
40
+ tracer = trace.get_tracer(__name__)
39
41
  odm_helper = OdmHelper(Hit)
40
42
 
41
43
 
44
+ @tracer.start_as_current_span(f"{__name__}.get_hit_workflow")
42
45
  def get_hit_workflow() -> Workflow:
43
46
  """Get the workflow that is used for transitioning between howler statuses
44
47
 
@@ -212,6 +215,7 @@ def get_hit_workflow() -> Workflow:
212
215
  )
213
216
 
214
217
 
218
+ @tracer.start_as_current_span(f"{__name__}._modifies_prop")
215
219
  def _modifies_prop(prop: str, operations: list[OdmUpdateOperation]) -> bool:
216
220
  """Check if the list of provided operations modifies the specified property
217
221
 
@@ -225,6 +229,7 @@ def _modifies_prop(prop: str, operations: list[OdmUpdateOperation]) -> bool:
225
229
  return any(op for op in operations if op.key == prop)
226
230
 
227
231
 
232
+ @tracer.start_as_current_span(f"{__name__}.does_hit_exist")
228
233
  def does_hit_exist(hit_id: str) -> bool:
229
234
  """Checks if the provided ID matches any entries in the database
230
235
 
@@ -237,6 +242,7 @@ def does_hit_exist(hit_id: str) -> bool:
237
242
  return datastore().hit.exists(hit_id)
238
243
 
239
244
 
245
+ @tracer.start_as_current_span(f"{__name__}.validate_hit_ids")
240
246
  def validate_hit_ids(hit_ids: list[str]) -> bool:
241
247
  """Checks if all hit_ids are available
242
248
 
@@ -249,6 +255,7 @@ def validate_hit_ids(hit_ids: list[str]) -> bool:
249
255
  return not any(does_hit_exist(hit_id) for hit_id in hit_ids)
250
256
 
251
257
 
258
+ @tracer.start_as_current_span(f"{__name__}.convert_hit")
252
259
  def convert_hit(data: dict[str, Any], unique: bool, ignore_extra_values: bool = False) -> tuple[Hit, list[str]]: # noqa: C901
253
260
  """Validate and convert a dictionary to a Hit ODM object.
254
261
 
@@ -354,6 +361,7 @@ def convert_hit(data: dict[str, Any], unique: bool, ignore_extra_values: bool =
354
361
  return odm, warnings
355
362
 
356
363
 
364
+ @tracer.start_as_current_span(f"{__name__}.exists")
357
365
  def exists(id: str):
358
366
  """Check if a hit exists in the datastore.
359
367
 
@@ -394,6 +402,7 @@ def get_hit(id: str, as_odm: Literal[False], version: Literal[False]) -> dict[st
394
402
  def get_hit(id: str, as_odm: Literal[False]) -> dict[str, Any]: ...
395
403
 
396
404
 
405
+ @tracer.start_as_current_span(f"{__name__}.get_hit")
397
406
  def get_hit(
398
407
  id: str,
399
408
  as_odm: bool = False,
@@ -420,6 +429,7 @@ CREATED_HITS = Counter(
420
429
  )
421
430
 
422
431
 
432
+ @tracer.start_as_current_span(f"{__name__}.create_hit")
423
433
  def create_hit(
424
434
  id: str,
425
435
  hit: Hit,
@@ -453,6 +463,7 @@ def create_hit(
453
463
  return datastore().hit.save(id, hit)
454
464
 
455
465
 
466
+ @tracer.start_as_current_span(f"{__name__}.update_hit")
456
467
  def update_hit(
457
468
  hit_id: str,
458
469
  operations: list[OdmUpdateOperation],
@@ -486,6 +497,7 @@ def update_hit(
486
497
 
487
498
 
488
499
  @typing.no_type_check
500
+ @tracer.start_as_current_span(f"{__name__}.save_hit")
489
501
  def save_hit(hit: Hit, version: Optional[str] = None) -> tuple[Hit, str]:
490
502
  """Save a hit to the datastore and emit an event notification.
491
503
 
@@ -506,6 +518,7 @@ def save_hit(hit: Hit, version: Optional[str] = None) -> tuple[Hit, str]:
506
518
  return data, _version
507
519
 
508
520
 
521
+ @tracer.start_as_current_span(f"{__name__}._update_hit")
509
522
  def _update_hit(
510
523
  hit_id: str,
511
524
  operations: list[OdmUpdateOperation],
@@ -593,6 +606,7 @@ def _update_hit(
593
606
  return data, _version
594
607
 
595
608
 
609
+ @tracer.start_as_current_span(f"{__name__}.get_transitions")
596
610
  def get_transitions(status: HitStatus) -> list[str]:
597
611
  """Get a list of the valid transitions beginning from the specified status
598
612
 
@@ -605,6 +619,7 @@ def get_transitions(status: HitStatus) -> list[str]:
605
619
  return get_hit_workflow().get_transitions(status)
606
620
 
607
621
 
622
+ @tracer.start_as_current_span(f"{__name__}.get_all_children")
608
623
  def get_all_children(hit: dict[str, Any]) -> list[dict[str, Any]]:
609
624
  """Get a list of all child hits for a given hit, including nested children.
610
625
 
@@ -632,6 +647,7 @@ def get_all_children(hit: dict[str, Any]) -> list[dict[str, Any]]:
632
647
  return child_hits
633
648
 
634
649
 
650
+ @tracer.start_as_current_span(f"{__name__}.transition_hit")
635
651
  def transition_hit(
636
652
  id: str,
637
653
  transition: HitStatusTransition,
@@ -736,6 +752,7 @@ def transition_hit(
736
752
  DELETED_HITS = Counter(f"{APP_NAME.replace('-', '_')}_deleted_hits_total", "The number of deleted hits")
737
753
 
738
754
 
755
+ @tracer.start_as_current_span(f"{__name__}.delete_hits")
739
756
  def delete_hits(hit_ids: list[str]) -> bool:
740
757
  """Delete a set of hits from the database
741
758
 
@@ -791,6 +808,7 @@ def search(
791
808
  ) -> SearchResult[dict[str, Any]]: ...
792
809
 
793
810
 
811
+ @tracer.start_as_current_span(f"{__name__}.search")
794
812
  def search(
795
813
  query: str,
796
814
  as_obj: bool = True,
@@ -1,8 +1,8 @@
1
1
  from typing import Any, Literal, Optional, overload
2
2
 
3
3
  from authlib.integrations.flask_client import OAuth
4
- from elasticapm.traces import capture_span
5
4
  from flask import current_app, request
5
+ from opentelemetry import trace
6
6
 
7
7
  from howler.common.exceptions import AccessDeniedException, HowlerValueError, InvalidDataException
8
8
  from howler.common.loader import datastore
@@ -16,6 +16,7 @@ from howler.utils.str_utils import safe_str
16
16
  ACCOUNT_USER_MODIFIABLE = ["name", "email", "avatar", "password", "dashboard", "refresh_rate"]
17
17
 
18
18
  logger = get_logger(__file__)
19
+ tracer = trace.get_tracer(__name__)
19
20
 
20
21
 
21
22
  @overload
@@ -89,7 +90,7 @@ def convert_user(user: User) -> dict[str, Any]:
89
90
  return user_data
90
91
 
91
92
 
92
- @capture_span(span_type="authentication")
93
+ @tracer.start_as_current_span(f"{__name__}.parse_user_data")
93
94
  def parse_user_data( # noqa: C901
94
95
  data: dict,
95
96
  oauth_provider: str,
@@ -0,0 +1,65 @@
1
+ """Telemetry setup for Howler API."""
2
+
3
+ import os
4
+
5
+ from flask import Flask
6
+
7
+ from howler.common.logging import get_logger
8
+
9
+ logger = get_logger(__file__)
10
+
11
+
12
+ def setup_telemetry(app: Flask) -> None:
13
+ """Initialize telemetry and library instrumentors.
14
+
15
+ The backend is selected via ``config.core.telemetry.backend``.
16
+
17
+ For ``opentelemetry``, a TracerProvider with the OTLP exporter is created
18
+ manually. Environment variables control the exporter:
19
+ - OTEL_EXPORTER_OTLP_ENDPOINT
20
+ - OTEL_EXPORTER_OTLP_HEADERS
21
+
22
+ See https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
23
+
24
+ For ``azure_monitor``, ``configure_azure_monitor`` from
25
+ ``azure-monitor-opentelemetry`` handles the full setup (provider,
26
+ exporters, and instrumentation). Requires the
27
+ ``APPLICATIONINSIGHTS_CONNECTION_STRING`` environment variable.
28
+ """
29
+ from howler.odm.models.config import config
30
+
31
+ backend = config.core.telemetry.backend
32
+
33
+ try:
34
+ if backend == "opentelemetry":
35
+ from opentelemetry import trace
36
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
37
+ from opentelemetry.instrumentation.flask import FlaskInstrumentor
38
+ from opentelemetry.sdk.resources import Resource
39
+ from opentelemetry.sdk.trace import TracerProvider
40
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
41
+
42
+ resource = Resource.create({"service.name": "howler-api"})
43
+ provider = TracerProvider(resource=resource)
44
+ provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
45
+ trace.set_tracer_provider(provider)
46
+ FlaskInstrumentor().instrument_app(app)
47
+ elif backend == "azure_monitor":
48
+ from azure.monitor.opentelemetry import configure_azure_monitor
49
+ from opentelemetry.instrumentation.flask import FlaskInstrumentor
50
+
51
+ if not os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING"):
52
+ logger.error(
53
+ "Azure Monitor telemetry backend selected but APPLICATIONINSIGHTS_CONNECTION_STRING is not set."
54
+ )
55
+ return
56
+
57
+ configure_azure_monitor()
58
+ FlaskInstrumentor().instrument_app(app)
59
+ else:
60
+ logger.error("Unsupported telemetry backend '%s'.", backend)
61
+ return
62
+
63
+ logger.info("Telemetry configured successfully (backend=%s).", backend)
64
+ except Exception:
65
+ logger.exception("Failed to configure telemetry (backend=%s).", backend)
@@ -152,7 +152,7 @@ suppress-none-returning = true
152
152
  [tool.poetry]
153
153
  package-mode = true
154
154
  name = "howler-api"
155
- version = "3.3.0.dev721"
155
+ version = "3.3.0.dev735"
156
156
  description = "Howler - API server"
157
157
  authors = [
158
158
  "Canadian Centre for Cyber Security <howler@cyber.gc.ca>",
@@ -199,7 +199,6 @@ python = "^3.9.17"
199
199
  apscheduler = "3.11.2"
200
200
  authlib = "^1.6.0"
201
201
  chardet = "5.2.0"
202
- elastic-apm = { extras = ["flask"], version = "^6.22.0" }
203
202
  elasticsearch = "8.19.3"
204
203
  flask = "3.1.3"
205
204
  flask-caching = "2.3.1"
@@ -229,6 +228,10 @@ pydash = "^8.0.5"
229
228
  pytz = "^2025.2"
230
229
  bcrypt = "4.3.0"
231
230
  tzdata = "^2026.1"
231
+ opentelemetry-sdk = "1.40.0"
232
+ opentelemetry-exporter-otlp-proto-http = "1.40.0"
233
+ opentelemetry-instrumentation-flask = "0.61b0"
234
+ azure-monitor-opentelemetry = "^1.8.7"
232
235
 
233
236
  [tool.poetry.group.dev.dependencies]
234
237
  pre-commit = "^3.7.0"