Repository: open-telemetry/opentelemetry-python

Issue:
Add support for `OTEL_EXPORTER_OTLP_LOGS_*` env vars
Similar to traces and metrics, the OTLP metric exporter should support `OTEL_EXPORTER_OTLP_LOGS_*` env vars.

Exporter protocol specification https://github.com/open-telemetry/opentelemetry-specification/blob/ab70eb9d364e7d6f25ba45e4ccbebaadceb5fa6a/specification/protocol/exporter.md


Discussion:
would like to take this up @srikanthccv 
@sudhanshu-chauhan are you still working on this?
@sudhanshu-chauhan Since there has been no progress for the last 45 days, and you haven't responded, I assume you don't have the time to complete it. I am unassigning you. If you want to pick up something again, there is always more to do. 
@srikanthccv  Can I take this one up?
`Similar to traces and metrics, the OTLP metric exporter should support ` Does it intend to say `OTLP log exporter`?
Traces, metrics, and logs are the 3 metrics, as I understand by working on my previous PR.
>Does it intend to say OTLP log exporter?

You correct. They are already supported for traces and metrics. This issue is for adding the same to the OTLP log exporter. We need to add support in two places
1. gRPC exporter (https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter)
2. HTTP exporter (https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter)

Look at how `OTEL_EXPORTER_OTLP_TRACES_*` or `OTEL_EXPORTER_OTLP_METRICS_*` are implemented and do the same. Let me know if you need any other help.
Thanks @srikanthccv Taking this one up first. 

Resolving patch:
diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py
--- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py
@@ -11,12 +11,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from os import environ
 from typing import Optional, Sequence
 from grpc import ChannelCredentials, Compression
 from opentelemetry.exporter.otlp.proto.grpc.exporter import (
     OTLPExporterMixin,
     get_resource_data,
+    _get_credentials,
     _translate_value,
+    environ_to_compression,
 )
 from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import (
     ExportLogsServiceRequest,
@@ -34,6 +37,15 @@
 from opentelemetry.sdk._logs import LogData
 from opentelemetry.sdk._logs.export import LogExporter, LogExportResult
 
+from opentelemetry.sdk.environment_variables import (
+    OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
+    OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
+    OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
+    OTEL_EXPORTER_OTLP_LOGS_HEADERS,
+    OTEL_EXPORTER_OTLP_LOGS_INSECURE,
+    OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
+)
+
 
 class OTLPLogExporter(
     LogExporter,
@@ -52,13 +64,40 @@ def __init__(
         timeout: Optional[int] = None,
         compression: Optional[Compression] = None,
     ):
+        if insecure is None:
+            insecure = environ.get(OTEL_EXPORTER_OTLP_LOGS_INSECURE)
+            if insecure is not None:
+                insecure = insecure.lower() == "true"
+
+        if (
+            not insecure
+            and environ.get(OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE) is not None
+        ):
+            credentials = _get_credentials(
+                credentials, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE
+            )
+
+        environ_timeout = environ.get(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT)
+        environ_timeout = (
+            int(environ_timeout) if environ_timeout is not None else None
+        )
+
+        compression = (
+            environ_to_compression(OTEL_EXPORTER_OTLP_LOGS_COMPRESSION)
+            if compression is None
+            else compression
+        )
+        endpoint = endpoint or environ.get(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)
+
+        headers = headers or environ.get(OTEL_EXPORTER_OTLP_LOGS_HEADERS)
+
         super().__init__(
             **{
                 "endpoint": endpoint,
                 "insecure": insecure,
                 "credentials": credentials,
                 "headers": headers,
-                "timeout": timeout,
+                "timeout": timeout or environ_timeout,
                 "compression": compression,
             }
         )
diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py
--- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py
@@ -29,6 +29,11 @@
     OTEL_EXPORTER_OTLP_ENDPOINT,
     OTEL_EXPORTER_OTLP_HEADERS,
     OTEL_EXPORTER_OTLP_TIMEOUT,
+    OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
+    OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
+    OTEL_EXPORTER_OTLP_LOGS_HEADERS,
+    OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
+    OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
 )
 from opentelemetry.sdk._logs import LogData
 from opentelemetry.sdk._logs.export import (
@@ -79,16 +84,26 @@ def __init__(
         compression: Optional[Compression] = None,
         session: Optional[requests.Session] = None,
     ):
-        self._endpoint = endpoint or _append_logs_path(
-            environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT)
+        self._endpoint = endpoint or environ.get(
+            OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
+            _append_logs_path(
+                environ.get(OTEL_EXPORTER_OTLP_ENDPOINT, DEFAULT_ENDPOINT)
+            ),
         )
         self._certificate_file = certificate_file or environ.get(
-            OTEL_EXPORTER_OTLP_CERTIFICATE, True
+            OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
+            environ.get(OTEL_EXPORTER_OTLP_CERTIFICATE, True),
+        )
+        headers_string = environ.get(
+            OTEL_EXPORTER_OTLP_LOGS_HEADERS,
+            environ.get(OTEL_EXPORTER_OTLP_HEADERS, ""),
         )
-        headers_string = environ.get(OTEL_EXPORTER_OTLP_HEADERS, "")
         self._headers = headers or parse_env_headers(headers_string)
         self._timeout = timeout or int(
-            environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT)
+            environ.get(
+                OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
+                environ.get(OTEL_EXPORTER_OTLP_TIMEOUT, DEFAULT_TIMEOUT),
+            )
         )
         self._compression = compression or _compression_from_env()
         self._session = session or requests.Session()
@@ -170,7 +185,12 @@ def shutdown(self):
 
 def _compression_from_env() -> Compression:
     compression = (
-        environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none").lower().strip()
+        environ.get(
+            OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
+            environ.get(OTEL_EXPORTER_OTLP_COMPRESSION, "none"),
+        )
+        .lower()
+        .strip()
     )
     return Compression(compression)
 
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py
@@ -344,6 +344,15 @@
 A scheme of https indicates a secure connection and takes precedence over this configuration setting.
 """
 
+OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"
+"""
+.. envvar:: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
+
+The :envvar:`OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` target to which the log exporter is going to send logs.
+The endpoint MUST be a valid URL host, and MAY contain a scheme (http or https), port and path.
+A scheme of https indicates a secure connection and takes precedence over this configuration setting.
+"""
+
 OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE"
 """
 .. envvar:: OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE
@@ -378,6 +387,14 @@
 associated with gRPC or HTTP requests.
 """
 
+OTEL_EXPORTER_OTLP_LOGS_HEADERS = "OTEL_EXPORTER_OTLP_LOGS_HEADERS"
+"""
+.. envvar:: OTEL_EXPORTER_OTLP_LOGS_HEADERS
+
+The :envvar:`OTEL_EXPORTER_OTLP_LOGS_HEADERS` contains the key-value pairs to be used as headers for logs
+associated with gRPC or HTTP requests.
+"""
+
 OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION"
 """
 .. envvar:: OTEL_EXPORTER_OTLP_TRACES_COMPRESSION
@@ -396,6 +413,14 @@
 exporter. If both are present, this takes higher precedence.
 """
 
+OTEL_EXPORTER_OTLP_LOGS_COMPRESSION = "OTEL_EXPORTER_OTLP_LOGS_COMPRESSION"
+"""
+.. envvar:: OTEL_EXPORTER_OTLP_LOGS_COMPRESSION
+
+Same as :envvar:`OTEL_EXPORTER_OTLP_COMPRESSION` but only for the log
+exporter. If both are present, this takes higher precedence.
+"""
+
 OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT"
 """
 .. envvar:: OTEL_EXPORTER_OTLP_TRACES_TIMEOUT
@@ -421,6 +446,15 @@
 Default: False
 """
 
+OTEL_EXPORTER_OTLP_LOGS_INSECURE = "OTEL_EXPORTER_OTLP_LOGS_INSECURE"
+"""
+.. envvar:: OTEL_EXPORTER_OTLP_LOGS_INSECURE
+
+The :envvar:`OTEL_EXPORTER_OTLP_LOGS_INSECURE` represents whether to enable client transport security
+for gRPC requests for metrics. A scheme of https takes precedence over the this configuration setting.
+Default: False
+"""
+
 OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"
 """
 .. envvar:: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
@@ -440,6 +474,14 @@
 TLS credentials of gRPC client for traces. Should only be used for a secure connection for tracing.
 """
 
+OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE = "OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE"
+"""
+.. envvar:: OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE
+
+The :envvar:`OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE` stores the path to the certificate file for
+TLS credentials of gRPC client for traces. Should only be used for a secure connection for tracing.
+"""
+
 OTEL_EXPORTER_OTLP_METRICS_HEADERS = "OTEL_EXPORTER_OTLP_METRICS_HEADERS"
 """
 .. envvar:: OTEL_EXPORTER_OTLP_METRICS_HEADERS
@@ -456,6 +498,14 @@
 wait for each batch export for metrics.
 """
 
+OTEL_EXPORTER_OTLP_LOGS_TIMEOUT = "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT"
+"""
+.. envvar:: OTEL_EXPORTER_OTLP_LOGS_TIMEOUT
+
+The :envvar:`OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` is the maximum time the OTLP exporter will
+wait for each batch export for logs.
+"""
+
 OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = (
     "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION"
 )


Test patch:
diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py
--- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py
@@ -14,12 +14,13 @@
 
 import time
 from concurrent.futures import ThreadPoolExecutor
+from os.path import dirname
 from unittest import TestCase
 from unittest.mock import patch
 
 from google.protobuf.duration_pb2 import Duration
 from google.rpc.error_details_pb2 import RetryInfo
-from grpc import StatusCode, server
+from grpc import ChannelCredentials, Compression, StatusCode, server
 
 from opentelemetry._logs import SeverityNumber
 from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
@@ -47,10 +48,19 @@
 )
 from opentelemetry.sdk._logs import LogData, LogRecord
 from opentelemetry.sdk._logs.export import LogExportResult
+from opentelemetry.sdk.environment_variables import (
+    OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
+    OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
+    OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
+    OTEL_EXPORTER_OTLP_LOGS_HEADERS,
+    OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
+)
 from opentelemetry.sdk.resources import Resource as SDKResource
 from opentelemetry.sdk.util.instrumentation import InstrumentationScope
 from opentelemetry.trace import TraceFlags
 
+THIS_DIR = dirname(__file__)
+
 
 class LogsServiceServicerUNAVAILABLEDelay(LogsServiceServicer):
     # pylint: disable=invalid-name,unused-argument,no-self-use
@@ -100,7 +110,6 @@ def Export(self, request, context):
 
 class TestOTLPLogExporter(TestCase):
     def setUp(self):
-
         self.exporter = OTLPLogExporter()
 
         self.server = server(ThreadPoolExecutor(max_workers=10))
@@ -164,6 +173,32 @@ def test_exporting(self):
         # pylint: disable=protected-access
         self.assertEqual(self.exporter._exporting, "logs")
 
+    @patch.dict(
+        "os.environ",
+        {
+            OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "logs:4317",
+            OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: THIS_DIR
+            + "/../fixtures/test.cert",
+            OTEL_EXPORTER_OTLP_LOGS_HEADERS: " key1=value1,KEY2 = VALUE=2",
+            OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "10",
+            OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: "gzip",
+        },
+    )
+    @patch(
+        "opentelemetry.exporter.otlp.proto.grpc.exporter.OTLPExporterMixin.__init__"
+    )
+    def test_env_variables(self, mock_exporter_mixin):
+        OTLPLogExporter()
+
+        self.assertTrue(len(mock_exporter_mixin.call_args_list) == 1)
+        _, kwargs = mock_exporter_mixin.call_args_list[0]
+        self.assertEqual(kwargs["endpoint"], "logs:4317")
+        self.assertEqual(kwargs["headers"], " key1=value1,KEY2 = VALUE=2")
+        self.assertEqual(kwargs["timeout"], 10)
+        self.assertEqual(kwargs["compression"], Compression.Gzip)
+        self.assertIsNotNone(kwargs["credentials"])
+        self.assertIsInstance(kwargs["credentials"], ChannelCredentials)
+
     @patch(
         "opentelemetry.exporter.otlp.proto.grpc.exporter.ssl_channel_credentials"
     )
diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py
--- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py
@@ -61,6 +61,11 @@
     OTEL_EXPORTER_OTLP_COMPRESSION,
     OTEL_EXPORTER_OTLP_ENDPOINT,
     OTEL_EXPORTER_OTLP_HEADERS,
+    OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE,
+    OTEL_EXPORTER_OTLP_LOGS_COMPRESSION,
+    OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
+    OTEL_EXPORTER_OTLP_LOGS_HEADERS,
+    OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
     OTEL_EXPORTER_OTLP_TIMEOUT,
 )
 from opentelemetry.sdk.resources import Resource as SDKResource
@@ -92,6 +97,38 @@ def test_constructor_default(self):
             "application/x-protobuf",
         )
 
+    @patch.dict(
+        "os.environ",
+        {
+            OTEL_EXPORTER_OTLP_CERTIFICATE: ENV_CERTIFICATE,
+            OTEL_EXPORTER_OTLP_COMPRESSION: Compression.Gzip.value,
+            OTEL_EXPORTER_OTLP_ENDPOINT: ENV_ENDPOINT,
+            OTEL_EXPORTER_OTLP_HEADERS: ENV_HEADERS,
+            OTEL_EXPORTER_OTLP_TIMEOUT: ENV_TIMEOUT,
+            OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE: "logs/certificate.env",
+            OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: Compression.Deflate.value,
+            OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "https://logs.endpoint.env",
+            OTEL_EXPORTER_OTLP_LOGS_HEADERS: "logsEnv1=val1,logsEnv2=val2,logsEnv3===val3==",
+            OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: "40",
+        },
+    )
+    def test_exporter_metrics_env_take_priority(self):
+        exporter = OTLPLogExporter()
+
+        self.assertEqual(exporter._endpoint, "https://logs.endpoint.env")
+        self.assertEqual(exporter._certificate_file, "logs/certificate.env")
+        self.assertEqual(exporter._timeout, 40)
+        self.assertIs(exporter._compression, Compression.Deflate)
+        self.assertEqual(
+            exporter._headers,
+            {
+                "logsenv1": "val1",
+                "logsenv2": "val2",
+                "logsenv3": "==val3==",
+            },
+        )
+        self.assertIsInstance(exporter._session, requests.Session)
+
     @patch.dict(
         "os.environ",
         {