Repository: dbt-labs/dbt-core

Issue:
[CT-2259] Avoid serialization error of `Undefined` with JSON-formatted logs
**Motivation: Commands should succeed or error consistently, regardless of the `--log-format` being used.** This is a prerequisite for changing how `dbt-core` is deployed in dbt Cloud.

Let's say I, as a user, try to log a variable that I have not defined:

```sql
-- models/my_model.sql

{{ log("I am here", info=True) }}

{{ log(unknown_variable_name_here, info=True) }}

{{ log("I am now here", info=True) }}

select 1 as id
```

Rather than raising an exception, dbt renders `unknown_variable_name_here` to the Jinja type `Undefined`. This is fine on all versions with text-formatting logging. Unfortunately, on older versions, when run with JSON-formatted logging, it yields this error:
```json
{
  "code": "Z028",
  "data": {
    "msg": "Object of type Undefined is not JSON serializable"
  },
  "invocation_id": "99db7ab4-cc63-4a0e-bb1f-30741bed654c",
  "level": "error",
  "log_version": 2,
  "msg": "\u001b[33mObject of type Undefined is not JSON serializable\u001b[0m",
  "pid": 41046,
  "thread_name": "MainThread",
  "ts": "2023-03-02T20:55:45.427322Z",
  "type": "log_line"
}
```

The good news is, this is handled gracefully in v1.4+, thanks to changes we made to log serialization:
```json
{
  "data": {
    "msg": "",
    "node_info": {
      "materialized": "view",
      "meta": {},
      "node_finished_at": null,
      "node_name": "my_failing_model",
      "node_path": "my_failing_model.sql",
      "node_started_at": "2023-03-02T20:56:46.648597",
      "node_status": "compiling",
      "resource_type": "model",
      "unique_id": "model.test.my_failing_model"
    }
  },
  "info": {
    "category": "",
    "code": "M011",
    "extra": {},
    "invocation_id": "a2e17f17-5036-4b0a-848f-80fe2c1283dd",
    "level": "info",
    "msg": "",
    "name": "JinjaLogInfo",
    "pid": 41211,
    "thread": "Thread-1",
    "ts": "2023-03-02T20:56:46.653561Z"
  }
}
```

So we just need a fix for older versions under critical support (v1.1, v1.2, v1.3).


## Proposed fix

It's a bit ugly, but it's also very very narrow in scope. Let's update the [`log` context method](https://github.com/dbt-labs/dbt-core/blob/main/core/dbt/context/base.py#L543-L562) to handle if the user has passed in an object that renders to type `Undefined`, and replace it with an empty string:
```python
        import jinja2
        if isinstance(msg, jinja2.Undefined):
            msg = ""
        if info:
            fire_event(MacroEventInfo(msg=str(msg)))
        else:
            fire_event(MacroEventDebug(msg=str(msg)))
        return ""
```
The effect is identical for text-formatted logs, and avoids serialization errors for JSON-formatted logs.

(In general, we should really stringify whatever the user has passed us - this would also solve for https://github.com/dbt-labs/dbt-core/issues/6568)

## Acceptance criteria
- [ ] Avoid serialization error for `Undefined` object being passed into `{{ log() }}` on v1.1, v1.2, v1.3
- [ ] Add a test for this behavior, on affected versions and going forward


Discussion:
There is also an issue with errors being thrown by user invocation of the "tojson" method, so we should probably do a similar thing of catching all errors and issuing a warning.

Resolving patch:
diff --git a/core/dbt/context/base.py b/core/dbt/context/base.py
--- a/core/dbt/context/base.py
+++ b/core/dbt/context/base.py
@@ -556,6 +556,10 @@ def log(msg: str, info: bool = False) -> str:
               {{ log("Running some_macro: " ~ arg1 ~ ", " ~ arg2) }}
             {% endmacro %}"
         """
+
+        if not isinstance(msg, str):
+            msg = str(msg)
+
         if info:
             fire_event(MacroEventInfo(msg=msg))
         else:


Test patch:
diff --git a/test/unit/test_base_context.py b/test/unit/test_base_context.py
new file mode 100644
--- /dev/null
+++ b/test/unit/test_base_context.py
@@ -0,0 +1,22 @@
+import os
+
+from dbt.context.base import BaseContext
+from jinja2.runtime import Undefined
+
+
+class TestBaseContext:
+    def test_log_jinja_undefined(self):
+        # regression test for CT-2259
+        try:
+            os.environ["DBT_ENV_SECRET_LOG_TEST"] = "cats_are_cool"
+            BaseContext.log(msg=Undefined(), info=True)
+        except Exception as e:
+            assert False, f"Logging an jinja2.Undefined object raises an exception: {e}"
+
+    def test_log_with_dbt_env_secret(self):
+        # regression test for CT-1783
+        try:
+            os.environ["DBT_ENV_SECRET_LOG_TEST"] = "cats_are_cool"
+            BaseContext.log({"fact1": "I like cats"}, info=True)
+        except Exception as e:
+            assert False, f"Logging while a `DBT_ENV_SECRET` was set raised an exception: {e}"