Repository: elastic/apm-agent-python

Issue:
elasticapm.label fails when trying to warn that no transaction is present
**Describe the bug**:
https://github.com/elastic/apm-agent-python/blob/main/elasticapm/traces.py#L1092

When neither Transaction nor client is present, `elasticapm.label( . . .)` will fail while trying do print a warning.
This can happen for example when testing routes with fastapi.

Open Question would be whether the warning should be printed if no client is present.
I think it makes sense to do this since this is also the default from the config.

**To Reproduce**

```
import elasticapm

elasticapm.label(foo="foo")
```


**Environment (please complete the following information)**
- OS: Tested on Linux and Mac but should appear everywhere else
- Python version: Tested on 3.9/3.10
- Agent version: 6.10.1


Discussion:
apmmachine: 
## :grey_exclamation: Build Aborted
  
  > The PR is not allowed to run in the CI yet
  


<!-- BUILD BADGES-->
> _the below badges are clickable and redirect to their specific view in the CI or DOCS_
[![Pipeline View](https://img.shields.io/badge/pipeline-pipeline%20-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-mbp%2FPR-1596/detail/PR-1596/1//pipeline) [![Test View](https://img.shields.io/badge/test-test-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-mbp%2FPR-1596/detail/PR-1596/1//tests) [![Changes](https://img.shields.io/badge/changes-changes-green)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-mbp%2FPR-1596/detail/PR-1596/1//changes) [![Artifacts](https://img.shields.io/badge/artifacts-artifacts-yellow)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-mbp%2FPR-1596/detail/PR-1596/1//artifacts)   [![preview](https://img.shields.io/badge/elastic-observability-blue)](https://ci-stats.elastic.co/app/apm/services/apm-ci/transactions/view?rangeFrom=2022-07-25T10:00:14.927Z&rangeTo=2022-07-25T10:20:14.927Z&transactionName=BUILD+apm-agent-python%2Fapm-agent-python-mbp%2FPR-%7Bnumber%7D&transactionType=job&latencyAggregationType=avg&traceId=edb3df28a0d3ac61d0ffae42f4ff5eac&transactionId=0ca727d0749c46c8)

<!-- BUILD SUMMARY-->
<details><summary>Expand to view the summary</summary>
<p>

#### Build stats




* Start Time: 2022-07-25T10:10:14.927+0000


* Duration: 3 min 24 sec





</p>
</details>

<!-- TEST RESULTS IF ANY-->


<!-- STEPS ERRORS IF ANY -->














### Steps errors [![2](https://img.shields.io/badge/2%20-red)](https://apm-ci.elastic.co/blue/organizations/jenkins/apm-agent-python%2Fapm-agent-python-mbp%2FPR-1596/detail/PR-1596/1//pipeline)
  <details><summary>Expand to view the steps failures</summary>
  <p>


  
  
  
  
##### `Load a resource file from a shared library`
<ul>
<li>Took 0 min 0 sec . View more details <a href="https://apm-ci.elastic.co//blue/rest/organizations/jenkins/pipelines/apm-agent-python/pipelines/apm-agent-python-mbp/pipelines/PR-1596/runs/1/steps/87/log/?start=0">here</a></li>
<li>Description: <code>approval-list/elastic/apm-agent-python.yml</code></l1>
</ul>
  
  
  
  
##### `Error signal`
<ul>
<li>Took 0 min 0 sec . View more details <a href="https://apm-ci.elastic.co//blue/rest/organizations/jenkins/pipelines/apm-agent-python/pipelines/apm-agent-python-mbp/pipelines/PR-1596/runs/1/steps/95/log/?start=0">here</a></li>
<li>Description: <code>githubApiCall: The REST API call https://api.github.com/orgs/elastic/members/dennis-wey return the message : java.lang.Exception: httpRequest: Failure connecting to the service https://api.github.com/orgs/elastic/members/dennis-wey : httpRequest: Failure connecting to the service https://api.github.com/orgs/elastic/members/dennis-wey : Code: 404Error: {"message":"User does not exist or is not a member of the organization","documentation_url":"https://docs.github.com/rest/reference/orgs#check-organization-membership-for-a-user"}</code></l1>
</ul>
  
  </p>
  </details>



## :robot: GitHub comments

To re-run your PR in the CI, just comment with:



- `/test` : Re-trigger the build.


- `/test linters` : Run the Python linters only.


- `/test full` : Run the full matrix of tests.


- `/test benchmark` : Run the APM Agent Python benchmarks tests.


- `run` `elasticsearch-ci/docs` : Re-trigger the docs validation. (use unformatted text in the comment!)




<!--COMMENT_GENERATED_WITH_ID_comment.id-->
apmmachine: ### :globe_with_meridians: Coverage report

Name | Metrics % (`covered/total`) | Diff
 --- | --- | ---
Packages | 100.0% (`68/68`) | :green_heart: 
Files | 100.0% (`227/227`) | :green_heart: 
Classes | 100.0% (`227/227`) | :green_heart: 
Lines | 89.644% (`17260/19254`) | :+1: 0.002
Conditionals | 76.869% (`3147/4094`) | :+1: 

<!--COMMENT_GENERATED_WITH_ID_coverage-->

Patch:
diff --git a/elasticapm/traces.py b/elasticapm/traces.py
index f7224e80..15dd9799 100644
--- a/elasticapm/traces.py
+++ b/elasticapm/traces.py
@@ -1089,7 +1089,8 @@ def label(**labels):
     """
     transaction = execution_context.get_transaction()
     if not transaction:
-        if elasticapm.get_client().config.enabled:
+        client = elasticapm.get_client()
+        if not client or client.config.enabled:
             error_logger.warning("Ignored labels %s. No transaction currently active.", ", ".join(labels.keys()))
     else:
         transaction.label(**labels)
diff --git a/elasticapm/utils/__init__.py b/elasticapm/utils/__init__.py
index 51f4e578..fb20c931 100644
--- a/elasticapm/utils/__init__.py
+++ b/elasticapm/utils/__init__.py
@@ -63,17 +63,15 @@ def varmap(func, var, context=None, name=None, **kwargs):
     if objid in context:
         return func(name, "<...>", **kwargs)
     context.add(objid)
-    if isinstance(var, dict):
-        # Apply func() before recursion, so that `shorten()` doesn't have to iterate over all the trimmed values
-        ret = func(name, var, **kwargs)
+
+    # Apply func() before recursion, so that `shorten()` doesn't have to iterate over all the trimmed values
+    ret = func(name, var, **kwargs)
+    if isinstance(ret, dict):
         # iterate over a copy of the dictionary to avoid "dictionary changed size during iteration" issues
         ret = dict((k, varmap(func, v, context, k, **kwargs)) for k, v in ret.copy().items())
-    elif isinstance(var, (list, tuple)):
+    elif isinstance(ret, (list, tuple)):
         # Apply func() before recursion, so that `shorten()` doesn't have to iterate over all the trimmed values
-        ret = func(name, var, **kwargs)
         ret = [varmap(func, f, context, name, **kwargs) for f in ret]
-    else:
-        ret = func(name, var, **kwargs)
     context.remove(objid)
     return ret
 


Test patch:
diff --git a/tests/client/client_tests.py b/tests/client/client_tests.py
index e130fef6..e7d64bef 100644
--- a/tests/client/client_tests.py
+++ b/tests/client/client_tests.py
@@ -536,3 +536,7 @@ def test_check_server_version(elasticapm_client):
 )
 def test_user_agent(elasticapm_client, expected):
     assert elasticapm_client.get_user_agent() == "apm-agent-python/unknown (myapp{})".format(expected)
+
+
+def test_label_without_client():
+    elasticapm.label(foo="foo")