Repository: PrefectHQ/prefect

Issue:
Show-flow-logs gives AttributeError with Docker agent
## Description

Starting a docker agent with `--show-flow-logs` can lead to a run error:

```
[2020-09-09 13:21:44,568] ERROR - agent | Error while deploying flow: AttributeError("Can't pickle local object 'Agent.run_heartbeat_thread.<locals>.run'")
```

This makes it difficult to debug custom images when, for example, flows are stuck in a Submitted state.

Initial Slack discussion was here: https://prefect-community.slack.com/archives/C014Z8DPDSR/p1599658959061800

## Expected Behavior

Flow log should allow a user to deterimine why a custom Docker image is broken.

## Reproduction

There are four reproduction cases here: https://github.com/masonlr/prefect-logging-issue

* [01-minimal-working](https://github.com/masonlr/prefect-logging-issue/tree/master/01-minimal-working): is a working minimal custom image using `prefecthq/prefect:0.13.5-python3.8`
* [02-minimal-with-flow-logs-BROKEN](https://github.com/masonlr/prefect-logging-issue/tree/master/02-minimal-with-flow-logs-BROKEN): illustrates the above AttributeError when `--show-flow-logs` is applied
* [03-python-custom-image](https://github.com/masonlr/prefect-logging-issue/tree/master/03-python-custom-image): is a working image using a customised `python:3.8-slim` base image
* [04-ubuntu-custom-BROKEN](https://github.com/masonlr/prefect-logging-issue/tree/master/04-ubuntu-custom-BROKEN): is an example that I am trying to troubleshoot using a customised `ubuntu:20.04` base image


## Environment
<!-- Any additional information about your environment

Optionally run `prefect diagnostics` from the command line and paste the information here. -->
```
{
  "config_overrides": {},
  "env_vars": [],
  "system_information": {
    "platform": "macOS-10.15.5-x86_64-i386-64bit",
    "prefect_version": "0.13.5",
    "python_version": "3.8.3"
  }
}
```


Discussion:

Resolving patch:
diff --git a/src/prefect/agent/docker/agent.py b/src/prefect/agent/docker/agent.py
--- a/src/prefect/agent/docker/agent.py
+++ b/src/prefect/agent/docker/agent.py
@@ -16,6 +16,21 @@
     import docker
 
 
+def _stream_container_logs(base_url: str, container_id: str) -> None:
+    """
+    Stream container logs back to stdout
+
+    Args:
+        - base_url (str): URL for a Docker daemon server
+        - container_id (str): ID of a container to stream logs
+    """
+    import docker
+
+    client = docker.APIClient(base_url=base_url, version="auto")
+    for log in client.logs(container=container_id, stream=True, follow=True):
+        print(str(log, "utf-8").rstrip())
+
+
 class DockerAgent(Agent):
     """
     Agent which deploys flow runs locally as Docker containers. Information on using the
@@ -401,29 +416,28 @@ def deploy_flow(self, flow_run: GraphQLResult) -> str:
         self.docker_client.start(container=container.get("Id"))
 
         if self.show_flow_logs:
-            proc = multiprocessing.Process(
-                target=self.stream_container_logs,
-                kwargs={"container_id": container.get("Id")},
-            )
-
-            proc.start()
-            self.processes.append(proc)
+            self.stream_flow_logs(container.get("Id"))
 
         self.logger.debug("Docker container {} started".format(container.get("Id")))
 
         return "Container ID: {}".format(container.get("Id"))
 
-    def stream_container_logs(self, container_id: str) -> None:
-        """
-        Stream container logs back to stdout
+    def stream_flow_logs(self, container_id: str) -> None:
+        """Stream container logs back to stdout.
 
         Args:
-            - container_id (str): ID of a container to stream logs
+            - container_id (str): ID of container
         """
-        for log in self.docker_client.logs(
-            container=container_id, stream=True, follow=True
-        ):
-            print(str(log, "utf-8").rstrip())
+        # All arguments to multiprocessing.Process need to be pickleable
+        proc = multiprocessing.Process(
+            target=_stream_container_logs,
+            kwargs={
+                "base_url": self.base_url,
+                "container_id": container_id,
+            },
+        )
+        proc.start()
+        self.processes.append(proc)
 
     def populate_env_vars(self, flow_run: GraphQLResult) -> dict:
         """


Test patch:
diff --git a/tests/agent/test_docker_agent.py b/tests/agent/test_docker_agent.py
--- a/tests/agent/test_docker_agent.py
+++ b/tests/agent/test_docker_agent.py
@@ -1,9 +1,10 @@
+import pickle
 from unittest.mock import MagicMock
 
 import pytest
 
 from prefect import context
-from prefect.agent.docker import DockerAgent
+from prefect.agent.docker.agent import DockerAgent, _stream_container_logs
 from prefect.environments import LocalEnvironment
 from prefect.environments.storage import Docker, Local
 from prefect.utilities.configuration import set_temporary_config
@@ -539,9 +540,14 @@ def test_docker_agent_deploy_flow_show_flow_logs(monkeypatch, cloud_api):
         )
     )
 
-    process.assert_called_with(
-        target=agent.stream_container_logs, kwargs={"container_id": "container_id"}
+    process_kwargs = dict(
+        target=_stream_container_logs,
+        kwargs={"base_url": agent.base_url, "container_id": "container_id"},
     )
+    process.assert_called_with(**process_kwargs)
+    # Check all arguments to `multiprocessing.Process` are pickleable
+    assert pickle.loads(pickle.dumps(process_kwargs)) == process_kwargs
+
     assert len(agent.processes) == 1
     assert api.create_container.called
     assert api.start.called