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.
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/PKG-INFO +5 -2
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/socket.py +4 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/clue.py +15 -17
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/app.py +5 -10
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/collection.py +4 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/oauth.py +0 -2
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/config.py +21 -24
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/security/__init__.py +0 -8
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/security/utils.py +4 -2
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/auth_service.py +6 -4
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/config_service.py +4 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/event_service.py +3 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/hit_service.py +18 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/user_service.py +3 -2
- howler_api-3.3.0.dev735/howler/telemetry.py +65 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/pyproject.toml +5 -2
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/README.md +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/add_label.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/change_field.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/demote.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/example_plugin.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/prioritization.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/promote.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/remove_label.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/actions/transition.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/base.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/action.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/analytic.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/auth.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/configs.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/dossier.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/help.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/hit.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/notebook.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/overview.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/search.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/template.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/tool.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/user.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/api/v1/view.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/README.md +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/classification.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/classification.yml +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/exceptions.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/loader.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/logging/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/logging/audit.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/logging/format.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/net.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/net_static.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/random_user.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/common/swagger.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/config.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/cronjobs/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/cronjobs/retention.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/cronjobs/rules.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/README.md +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/bulk.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/constants.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/exceptions.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/howler_store.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/operations.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/schemas.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/store.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/support/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/support/build.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/support/schemas.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/datastore/types.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/error.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/README.md +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/generate_mitre.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/generate_tlds.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/reindex_data.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/external/wipe_databases.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/gunicorn_config.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/healthz.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/azure.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/discover.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/hit.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/search.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/workflow.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/helper/ws.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/README.md +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/base.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/charter.txt +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/helper.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/howler_enum.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/action.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/analytic.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/aws.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/azure.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/cbs.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/clue.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/dossier.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/gcp.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/hit.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/howler_data.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/lead.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/localized_label.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/overview.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/pivot.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/template.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/user.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/models/view.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/random_data.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/odm/randomizer.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/patched.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/plugins/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/plugins/config.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/README.md +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/events.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/set.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/security/socket.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/action_service.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/analytic_service.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/dossier_service.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/jwt_service.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/lucene_service.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/notebook_service.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/overview_service.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/services/template_service.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/__init__.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/annotations.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/chunk.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/dict_utils.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/isotime.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/list_utils.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/lucene.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/path.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/socket_utils.py +0 -0
- {howler_api-3.3.0.dev721 → howler_api-3.3.0.dev735}/howler/utils/str_utils.py +0 -0
- {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.
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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,
|
|
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
|
-
|
|
473
|
-
"Configuration for
|
|
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
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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.
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|