Skip to content

Commit 7e9291d

Browse files
Event Observer API (#384)
* event classes, basic observer wip 1, mini unit test * added utility functions for common event observer patterns * add event types for request and response * circ imp wip1 * apioptions management of observers + circ-imp refactor + unit tests * some wiring to the astrapy database classes * remove LOG observable event * event observers, full wiring * unit test for wired sync classes (not admin yet) * all nonadmin ev-obs tests in place * commander/observer attaching, testing complete * sender in ev-obs; wiring for function name in ev-obs * function name passed to ev-obs primitives * additional metadata in request/response events for observers * request id in observable events * refactor event-observer API into a subdir * context manager shorthand for capturing events * add test for event_collector * final testing and docstring for event_collector * stricter arg around request/apicommander for ev-obs * test-imports and README on ev-obs * readme edits for ev-obs * ev-obs: docstrings, last signature changes * Add dev_ops_api bool flag to ObservableRequest * changesfile and another note in the readme for ev-obs * add fixes local mypy did not see (?) --------- Co-authored-by: Stefano Lottini <236640031+sl-at-ibm@users.noreply.github.com>
1 parent 67126bd commit 7e9291d

24 files changed

Lines changed: 2317 additions & 337 deletions

CHANGES

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
main
22
====
3+
Event Observer API, to listen to events:
4+
- this complements the standard logging mechanism;
5+
- events are: warnings, payloads/responses, ...
6+
- useful for customized logging, observability and instrumentation;
7+
- see `astrapy.event_observers` module and docstring of classes therein for more.
38
mainteinance: enabled text-index integration tests on Astra
49
maintenance: introduced the publish-and-release workflow machinery
510
maintenance: split development/maintenance from user README

README.md

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -625,8 +625,9 @@ document = my_collection.find_one({"code": 123})
625625

626626
### Working with ObjectIds and UUIDs in Collections
627627

628-
Astrapy repackages the ObjectId from `bson` and the UUID class and utilities
629-
from the `uuid` package and its `uuidv6` extension. You can also use them directly.
628+
Astrapy repackages the ObjectId from PyMongo's `bson` and the UUID class and
629+
utilities from the `uuid` package and its `uuidv6` extension. You can also
630+
use them directly.
630631

631632
Even when setting a default ID type for a collection, you still retain the freedom
632633
to use any ID type for any document:
@@ -687,6 +688,116 @@ print(unescape_field_path("a&&&.b.c.d.12"))
687688
# prints: ['a&.b', 'c', 'd', '12']
688689
```
689690

691+
### Logging, instrumentation and observability
692+
693+
#### Logging
694+
695+
AstraPy's actions are logged using the standard `logging` facilities.
696+
To control the verbosity (logging level), a script preamble can have something
697+
like:
698+
699+
```python
700+
import logging
701+
logging.basicConfig(level=logging.DEBUG)
702+
```
703+
704+
#### Event observers with context manager
705+
706+
When logging does not suffice, you can use the **Event Observer API**, i.e. attach
707+
an observer to the main astrapy class hierarchy (clients, databases,
708+
tables/collections; plus the admin classes).
709+
710+
Events are, for example, errors and warnings, as well as requests sent
711+
and responses received.
712+
713+
Events are generally issued during the lifecycle of a Data API or DevOps API
714+
HTTP request. Note that events are in correspondence with individual requests,
715+
and not class methods, some of which in fact involve multiple requests (e.g. the
716+
chunked `insert_many` or the database admin's `create_database` with
717+
`wait_until_active=True`).
718+
719+
The simplest usage, when one needs to capture events for a limited
720+
span of statements, is through a context manager:
721+
722+
```python
723+
from astrapy.event_observers import event_collector, ObservableEvent
724+
725+
ev_lst: list[ObservableEvent] = []
726+
727+
with event_collector(db, destination=event_list) as instrumented_db:
728+
# Use instrumented_db: call its methods, spawn and use collections, etc:
729+
instrumented_table = instrumented_db.get_table("my_table")
730+
instrumented_table.insert_one({"id": 123, "name": "Fox"})
731+
732+
for event in event_list:
733+
print(event.event_type)
734+
```
735+
736+
For more details and options, see the docstring of
737+
the `event_collector` utility.
738+
739+
#### Event observers with utility factories
740+
741+
An event observer can be created explicitly and attached through the API Options.
742+
743+
```python
744+
from astrapy.api_options import APIOptions
745+
from astrapy.event_observers import (
746+
ObservableEvent,
747+
ObservableEventType,
748+
Observer,
749+
)
750+
751+
my_event_list: list[ObservableEvent] = []
752+
my_observer = Observer.from_event_list(
753+
my_event_list,
754+
# optional filtering by event type:
755+
event_types=[ObservableEventType.REQUEST, ObservableEventType.RESPONSE],
756+
)
757+
758+
instrumented_client = my_client.with_options(
759+
api_options=APIOptions(event_observers={"my_obs001": my_observer})
760+
)
761+
762+
# any event of the desired type emitted by 'instrumented_client',
763+
# or by classes it has spawned, will be accumulated into 'my_event_list'.
764+
```
765+
766+
See also method `Observer.from_event_dict` for a similar alternative.
767+
768+
This approach allows attaching multiple observers, if needed.
769+
770+
#### Fully customized event observer
771+
772+
For higher control, one can subclass `Observer` directly:
773+
774+
```python
775+
from typing import Any
776+
from astrapy.api_options import APIOptions
777+
from astrapy.event_observers import ObservableEvent, Observer
778+
779+
class MyObserver(Observer):
780+
def receive(
781+
self,
782+
event: ObservableEvent,
783+
sender: Any = None,
784+
function_name: str | None = None,
785+
request_id: str | None = None,
786+
) -> None:
787+
received_item = {
788+
"event_type": event.event_type.value,
789+
"sender": sender,
790+
"function_name": function_name,
791+
"request_id": request_id,
792+
}
793+
print(f"Just received: {received_item}")
794+
795+
my_observer = MyObserver()
796+
instrumented_collection = my_collection.with_options(
797+
api_options=APIOptions(event_observers={"custom_obs001": my_observer})
798+
)
799+
```
800+
690801
## Appendices
691802

692803
### Appendix A: quick reference for key imports
@@ -857,6 +968,21 @@ from astrapy.authentication import (
857968
)
858969
```
859970

971+
Event observer API:
972+
973+
```python
974+
from astrapy.event_observers import (
975+
event_collector,
976+
ObservableEventType,
977+
ObservableEvent,
978+
ObservableError,
979+
ObservableWarning,
980+
ObservableRequest,
981+
ObservableResponse,
982+
Observer,
983+
)
984+
```
985+
860986
Miscellaneous utilities:
861987

862988
```python

0 commit comments

Comments
 (0)