def instrument_object(
        self, obj, query: serial_utils.Lens, done: Optional[Set[int]] = None
    ):
        """Instrument the given object `obj` and its components."""

        done = done or set()

        cls = type(obj)

        mro = list(cls.__mro__)
        # Warning: cls.__mro__ sometimes returns an object that can be iterated through only once.

        logger.debug(
            "%s: instrumenting object at %s of class %s",
            query,
            python_utils.id_str(obj),
            python_utils.class_name(cls),
        )

        if id(obj) in done:
            logger.debug("\t%s: already instrumented", query)
            return

        done.add(id(obj))

        # NOTE: We cannot instrument chain directly and have to instead
        # instrument its class. The pydantic.BaseModel does not allow instance
        # attributes that are not fields:
        # https://github.com/pydantic/pydantic/blob/11079e7e9c458c610860a5776dc398a4764d538d/pydantic/main.py#LL370C13-L370C13
        # .

        # Recursively instrument inner components
        if hasattr(obj, "__dict__"):
            for attr_name, attr_value in obj.__dict__.items():
                if isinstance(
                    attr_value, python_utils.OpaqueWrapper
                ):  # never look past opaque wrapper
                    continue
                if any(
                    isinstance(attr_value, cls) for cls in self.include_classes
                ):
                    inner_query = query[attr_name]
                    self.instrument_object(attr_value, inner_query, done)

        for base in mro:
            # Some top part of mro() may need instrumentation here if some
            # subchains call superchains, and we want to capture the
            # intermediate steps. On the other hand we don't want to instrument
            # the very base classes such as object:
            # if not self.to_instrument_module(base.__module__):
            #    print(
            #        f"skipping base {base} because of module {base.__module__}"
            #    )
            #    continue

            try:
                if not self.to_instrument_class(base):
                    logger.debug(f"skipping base {base} because of class")
                    continue

            except Exception as e:
                # subclass check may raise exception
                logger.debug(
                    "\t\tWarning: checking whether %s should be instrumented resulted in an error: %s",
                    python_utils.module_name(base),
                    e,
                )
                # NOTE: Proceeding to instrument here as we don't want to miss
                # anything. Unsure why some llama_index subclass checks fail.

                # continue

            print(f"instrumenting {obj.__class__} for base {base}")

            for instrumented_method in self.include_methods:
                method_name = instrumented_method.method
                class_filter = instrumented_method.class_filter
                if python_utils.safe_hasattr(base, method_name):
                    if not class_filter_matches(f=class_filter, obj=obj):
                        continue

                    print("\tinstrumenting", method_name)
                    original_fun = getattr(base, method_name)

                    # If an instrument class uses a decorator to wrap one of
                    # their methods, the wrapper will capture an un-instrumented
                    # version of the inner method which we may fail to
                    # instrument.
                    # @davidkurokawa:
                    #   This doesn't make any sense. This will remove a non
                    #   trulens wrapper such as for `llama-index` functions
                    #   decorated by `@dispatcher.span`. I also don't
                    #   understand the comment above so it's hard to just
                    #   fix this easily, but given non-OTEL is soon to be
                    #   deprecated, I'm not going to fix this.
                    #   I've also changed this from an `if` to a `while` so
                    #   that it removes all decorators since that would make
                    #   slightly more sense and it also fixes a test that
                    #   had issues due to the non-OTEL flow trying to wrap a
                    #   function with a `@wrapt.decorator` decorator which is
                    #   incompatible with the way `@functools.wraps` works.
                    original_fun = inspect.unwrap(original_fun)

                    # Sometimes the base class may be in some module but when a
                    # method is looked up from it, it actually comes from some
                    # other, even baser class which might come from builtins
                    # which we want to skip instrumenting.
                    if python_utils.safe_hasattr(original_fun, "__self__"):
                        if not self.to_instrument_module(
                            original_fun.__self__.__class__.__module__
                        ):
                            continue
                    else:
                        # Determine module here somehow.
                        pass

                    logger.debug("\t\t%s: instrumenting %s", query, method_name)

                    setattr(
                        base,
                        method_name,
                        self.tracked_method_wrapper(
                            query=query,
                            func=original_fun,
                            method_name=method_name,
                            cls=base,
                            obj=obj,
                            span_type=instrumented_method.span_type,
                            attributes=instrumented_method.attributes,
                            must_be_first_wrapper=instrumented_method.must_be_first_wrapper,
                        ),
                    )

        if self.to_instrument_object(obj) or isinstance(
            obj, (dict, list, tuple)
        ):
            vals = None
            if isinstance(obj, dict):
                attrs = obj.keys()
                vals = obj.values()

            if isinstance(obj, pydantic.BaseModel):
                # NOTE(piotrm): This will not include private fields like
                # llama_index's LLMPredictor._llm which might be useful to
                # include:
                attrs = type(obj).model_fields.keys()

            if isinstance(obj, v1BaseModel):
                attrs = obj.__fields__.keys()

            elif dataclasses.is_dataclass(type(obj)):
                attrs = (f.name for f in dataclasses.fields(obj))

            else:
                # If an object is not a recognized container type, we check that it
                # is meant to be instrumented and if so, we  walk over it manually.
                # NOTE: some llama_index objects are using dataclasses_json but most do
                # not so this section applies.
                attrs = pyschema_utils.clean_attributes(
                    obj, include_props=True
                ).keys()

            if vals is None:
                vals = [
                    python_utils.safe_getattr(obj, k, get_prop=True)
                    for k in attrs
                ]

            for k, v in zip(attrs, vals):
                if isinstance(v, (str, bool, int, float)):
                    pass

                elif self.to_instrument_module(type(v).__module__):
                    self.instrument_object(obj=v, query=query[k], done=done)

                elif isinstance(v, Sequence):
                    for i, sv in enumerate(v):
                        if self.to_instrument_class(type(sv)):
                            self.instrument_object(
                                obj=sv, query=query[k][i], done=done
                            )

                elif isinstance(v, Dict):
                    for k2, sv in v.items():
                        subquery = query[k][k2]
                        if self.to_instrument_class(type(sv)):
                            self.instrument_object(
                                obj=sv, query=subquery, done=done
                            )

                else:
                    pass

                # TODO: check if we want to instrument anything in langchain not
                # accessible through model_fields .

        else:
            logger.debug(
                "%s: Do not know how to instrument object of type %s.",
                query,
                python_utils.class_name(cls),
            )

        # Check whether bound methods are instrumented properly.
Instrument the given object `obj` and its components.
src/core/trulens/core/instruments.py