Repository: dbt-labs/dbt-core

Issue:
[CT-756] Stringify user-provided messages to `{{ log() }}`
### Reproduction case

```sql
-- models/any_model.sql

{% set some_list = ['a', 'b', 'c'] %}

{{ log(some_list, info = true) }}

select 1 as id
```
```
$ DBT_ENV_SECRET_WHATEVER=1234 dbt run
12:12:30  Running with dbt=1.2.0-a1
12:12:30  Encountered an error:
'list' object has no attribute 'replace'
```

I'm not even using the secret, but because I've defined one, dbt is searching log messages to scrub out any instances of the secret value. This error is _not_ related to the recent change re: secret rendering. It's just related to how we're scrubbing secrets from log messages in general, and have been since v1.0.0:

https://github.com/dbt-labs/dbt-core/blob/6e8388c653c6bbeee934aea7db93730bd88df091/core/dbt/events/functions.py#L122-L128

When a user passes a non-`string` object into `{{ log() }}`, we end up passing that object straight through to `scrub_secrets`. Which expects to be receiving a string!

The type signature of `log` actually expects to be receiving a string, too:

https://github.com/dbt-labs/dbt-core/blob/6e8388c653c6bbeee934aea7db93730bd88df091/core/dbt/context/base.py#L543-L562

### Resolution

We could put this logic anywhere, but it feels like the simplest and most appropriate fix may well be to stringify whatever users are passing into `log`, before firing it:
```python
    def log(msg: str, info: bool = False) -> str:
        ...
        # user-provided msg should be a string
        msg = str(msg)
        if info:
            fire_event(MacroEventInfo(msg=msg))
        else:
            fire_event(MacroEventDebug(msg=msg))
        return ""
```

This successfully handles some pretty weird edge cases:
```sql
{{ log(this, info = true) }}
{{ log(create_table_as, info = true) }}
{{ log(context, info = true) }}
```
```
12:11:26  "jerco"."dbt_jcohen"."model_a"
12:11:26  <dbt.clients.jinja.MacroGenerator object at 0x10dca87f0>
... giant context blob ...
```

Still, we should make sure it raises a useful error, in case users try passing in objects that aren't string serializable. (I can't think of an example right now, but maybe there is one?)


Discussion:
@VersusFacit does this seem related to the ticket you just closed out? Should we solve this further down the stack? 
#5436 
#5874 

Resolving patch:
diff --git a/core/dbt/events/adapter_endpoint.py b/core/dbt/events/adapter_endpoint.py
--- a/core/dbt/events/adapter_endpoint.py
+++ b/core/dbt/events/adapter_endpoint.py
@@ -9,6 +9,7 @@
 )
 
 
+# N.B. No guarantees for what type param msg is.
 @dataclass
 class AdapterLogger:
     name: str
diff --git a/core/dbt/events/base_types.py b/core/dbt/events/base_types.py
--- a/core/dbt/events/base_types.py
+++ b/core/dbt/events/base_types.py
@@ -99,6 +99,23 @@ def level_tag(self) -> str:
         return "error"
 
 
+# Included to ensure classes with str-type message members are initialized correctly.
+@dataclass  # type: ignore[misc]
+class AdapterEventStringFunctor:
+    def __post_init__(self):
+        super().__post_init__()
+        if not isinstance(self.base_msg, str):
+            self.base_msg = str(self.base_msg)
+
+
+@dataclass  # type: ignore[misc]
+class EventStringFunctor:
+    def __post_init__(self):
+        super().__post_init__()
+        if not isinstance(self.msg, str):
+            self.msg = str(self.msg)
+
+
 # prevents an event from going to the file
 # This should rarely be used in core code. It is currently
 # only used in integration tests and for the 'clean' command.
diff --git a/core/dbt/events/types.py b/core/dbt/events/types.py
--- a/core/dbt/events/types.py
+++ b/core/dbt/events/types.py
@@ -7,6 +7,8 @@
     WarnLevel,
     ErrorLevel,
     Cache,
+    AdapterEventStringFunctor,
+    EventStringFunctor,
 )
 from dbt.events.format import format_fancy_output_line, pluralize
 
@@ -309,7 +311,7 @@ def message(self) -> str:
 
 
 @dataclass
-class AdapterEventDebug(DebugLevel, pt.AdapterEventDebug):  # noqa
+class AdapterEventDebug(DebugLevel, AdapterEventStringFunctor, pt.AdapterEventDebug):  # noqa
     def code(self):
         return "E001"
 
@@ -318,7 +320,7 @@ def message(self):
 
 
 @dataclass
-class AdapterEventInfo(InfoLevel, pt.AdapterEventInfo):  # noqa
+class AdapterEventInfo(InfoLevel, AdapterEventStringFunctor, pt.AdapterEventInfo):  # noqa
     def code(self):
         return "E002"
 
@@ -327,7 +329,7 @@ def message(self):
 
 
 @dataclass
-class AdapterEventWarning(WarnLevel, pt.AdapterEventWarning):  # noqa
+class AdapterEventWarning(WarnLevel, AdapterEventStringFunctor, pt.AdapterEventWarning):  # noqa
     def code(self):
         return "E003"
 
@@ -336,7 +338,7 @@ def message(self):
 
 
 @dataclass
-class AdapterEventError(ErrorLevel, pt.AdapterEventError):  # noqa
+class AdapterEventError(ErrorLevel, AdapterEventStringFunctor, pt.AdapterEventError):  # noqa
     def code(self):
         return "E004"
 
@@ -1218,7 +1220,9 @@ def message(self) -> str:
 
 # TODO: switch to storing structured info and calling get_target_failure_msg
 @dataclass
-class InvalidDisabledSourceInTestNode(WarnLevel, pt.InvalidDisabledSourceInTestNode):
+class InvalidDisabledSourceInTestNode(
+    WarnLevel, EventStringFunctor, pt.InvalidDisabledSourceInTestNode
+):
     def code(self):
         return "I050"
 
@@ -1227,7 +1231,7 @@ def message(self) -> str:
 
 
 @dataclass
-class InvalidRefInTestNode(DebugLevel, pt.InvalidRefInTestNode):
+class InvalidRefInTestNode(DebugLevel, EventStringFunctor, pt.InvalidRefInTestNode):
     def code(self):
         return "I051"
 
@@ -1334,7 +1338,7 @@ def message(self) -> str:
 
 
 @dataclass
-class MacroEventInfo(InfoLevel, pt.MacroEventInfo):
+class MacroEventInfo(InfoLevel, EventStringFunctor, pt.MacroEventInfo):
     def code(self):
         return "M011"
 
@@ -1343,7 +1347,7 @@ def message(self) -> str:
 
 
 @dataclass
-class MacroEventDebug(DebugLevel, pt.MacroEventDebug):
+class MacroEventDebug(DebugLevel, EventStringFunctor, pt.MacroEventDebug):
     def code(self):
         return "M012"
 
@@ -2261,7 +2265,7 @@ def message(self) -> str:
 
 
 @dataclass
-class RunResultError(ErrorLevel, pt.RunResultError):
+class RunResultError(ErrorLevel, EventStringFunctor, pt.RunResultError):
     def code(self):
         return "Z024"
 
@@ -2299,7 +2303,7 @@ def message(self) -> str:
 
 
 @dataclass
-class FirstRunResultError(ErrorLevel, pt.FirstRunResultError):
+class FirstRunResultError(ErrorLevel, EventStringFunctor, pt.FirstRunResultError):
     def code(self):
         return "Z028"
 
@@ -2308,7 +2312,7 @@ def message(self) -> str:
 
 
 @dataclass
-class AfterFirstRunResultError(ErrorLevel, pt.AfterFirstRunResultError):
+class AfterFirstRunResultError(ErrorLevel, EventStringFunctor, pt.AfterFirstRunResultError):
     def code(self):
         return "Z029"
 
@@ -2446,7 +2450,7 @@ def message(self) -> str:
 
 
 @dataclass
-class GeneralWarningMsg(WarnLevel, pt.GeneralWarningMsg):
+class GeneralWarningMsg(WarnLevel, EventStringFunctor, pt.GeneralWarningMsg):
     def code(self):
         return "Z046"
 
@@ -2476,7 +2480,7 @@ def message(self) -> str:
 
 
 @dataclass
-class RunResultWarningMessage(WarnLevel, pt.RunResultWarningMessage):
+class RunResultWarningMessage(WarnLevel, EventStringFunctor, pt.RunResultWarningMessage):
     def code(self):
         return "Z049"
 


Test patch: