From 1fc4ac652fcfedc67d1e6dbb1a22d345325baeee Mon Sep 17 00:00:00 2001
From: Abinet18 <35442169+Abinet18@users.noreply.github.com>
Date: Fri, 2 Jun 2023 00:58:44 -0700
Subject: [PATCH] feat(exporter-logs-otlp-proto): implements protobuf exporter
 for logs (#3779)

Co-authored-by: Marc Pichler <marc.pichler@dynatrace.com>
---
 experimental/CHANGELOG.md                     |   1 +
 .../exporter-logs-otlp-proto/.eslintignore    |   1 +
 .../exporter-logs-otlp-proto/.eslintrc.js     |   8 +
 .../exporter-logs-otlp-proto/.npmignore       |   4 +
 .../packages/exporter-logs-otlp-proto/LICENSE | 201 +++++++++
 .../exporter-logs-otlp-proto/README.md        | 110 +++++
 .../exporter-logs-otlp-proto/karma.conf.js    |  26 ++
 .../exporter-logs-otlp-proto/package.json     |  95 ++++
 .../exporter-logs-otlp-proto/src/index.ts     |  16 +
 .../src/platform/browser/OTLPLogsExporter.ts  |  76 ++++
 .../src/platform/browser/index.ts             |  16 +
 .../src/platform/index.ts                     |  16 +
 .../src/platform/node/OTLPLogsExporter.ts     |  76 ++++
 .../src/platform/node/index.ts                |  17 +
 .../test/browser/OTLPLogsExporter.test.ts     |  50 +++
 .../test/browser/index-webpack.ts             |  20 +
 .../test/logHelper.ts                         | 186 ++++++++
 .../test/node/OTLPLogsExporter.test.ts        | 416 ++++++++++++++++++
 .../tsconfig.esm.json                         |  40 ++
 .../tsconfig.esnext.json                      |  40 ++
 .../exporter-logs-otlp-proto/tsconfig.json    |  40 ++
 .../browser/OTLPProtoExporterBrowserBase.ts   |  21 +-
 .../src/platform/index.ts                     |   8 +-
 .../src/platform/node/index.ts                |   2 +-
 .../src/platform/node/util.ts                 |  21 +-
 .../src/platform/types.ts                     |   1 +
 .../src/platform/util.ts                      |  39 ++
 tsconfig.esm.json                             |   3 +
 tsconfig.esnext.json                          |   3 +
 tsconfig.json                                 |   4 +
 30 files changed, 1511 insertions(+), 46 deletions(-)
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/.eslintignore
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/.eslintrc.js
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/.npmignore
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/LICENSE
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/README.md
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/karma.conf.js
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/package.json
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/src/index.ts
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/src/platform/browser/OTLPLogsExporter.ts
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/src/platform/browser/index.ts
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/src/platform/index.ts
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/src/platform/node/OTLPLogsExporter.ts
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/src/platform/node/index.ts
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogsExporter.test.ts
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/test/browser/index-webpack.ts
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/test/logHelper.ts
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogsExporter.test.ts
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/tsconfig.esm.json
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/tsconfig.esnext.json
 create mode 100644 experimental/packages/exporter-logs-otlp-proto/tsconfig.json
 create mode 100644 experimental/packages/otlp-proto-exporter-base/src/platform/util.ts

diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md
index f5dbfd2a8fe..4935d9b83ba 100644
--- a/experimental/CHANGELOG.md
+++ b/experimental/CHANGELOG.md
@@ -19,6 +19,7 @@ All notable changes to experimental packages in this project will be documented
 * feat(otlp-trace-exporters): Add User-Agent header to OTLP trace exporters. [#3790](https://github.com/open-telemetry/opentelemetry-js/pull/3790) @JamieDanielson
 * feat(otlp-metric-exporters): Add User-Agent header to OTLP metric exporters. [#3806](https://github.com/open-telemetry/opentelemetry-js/pull/3806) @JamieDanielson
 * feat(opencensus-shim): add OpenCensus trace shim [#3809](https://github.com/open-telemetry/opentelemetry-js/pull/3809) @aabmass
+* feat(exporter-logs-otlp-proto): protobuf exporter for logs. [#3779](https://github.com/open-telemetry/opentelemetry-js/pull/3779) @Abinet18
 
 ### :bug: (Bug Fix)
 
diff --git a/experimental/packages/exporter-logs-otlp-proto/.eslintignore b/experimental/packages/exporter-logs-otlp-proto/.eslintignore
new file mode 100644
index 00000000000..378eac25d31
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/.eslintignore
@@ -0,0 +1 @@
+build
diff --git a/experimental/packages/exporter-logs-otlp-proto/.eslintrc.js b/experimental/packages/exporter-logs-otlp-proto/.eslintrc.js
new file mode 100644
index 00000000000..3ed0fbeba38
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/.eslintrc.js
@@ -0,0 +1,8 @@
+module.exports = {
+    "env": {
+        "mocha": true,
+        "commonjs": true,
+        "node": true,
+    },
+    ...require('../../../eslint.config.js')
+}
diff --git a/experimental/packages/exporter-logs-otlp-proto/.npmignore b/experimental/packages/exporter-logs-otlp-proto/.npmignore
new file mode 100644
index 00000000000..9505ba9450f
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/.npmignore
@@ -0,0 +1,4 @@
+/bin
+/coverage
+/doc
+/test
diff --git a/experimental/packages/exporter-logs-otlp-proto/LICENSE b/experimental/packages/exporter-logs-otlp-proto/LICENSE
new file mode 100644
index 00000000000..261eeb9e9f8
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/experimental/packages/exporter-logs-otlp-proto/README.md b/experimental/packages/exporter-logs-otlp-proto/README.md
new file mode 100644
index 00000000000..3019f50d3fb
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/README.md
@@ -0,0 +1,110 @@
+# An OTLP exporter to send logs using protobuf over HTTP
+
+[![NPM Published Version][npm-img]][npm-url]
+[![Apache License][license-image]][license-image]
+
+**Note: This is an experimental package under active development. New releases may include breaking changes.**
+
+This module provides an exporter for OTLP (http/protobuf) logs using protocol version `v0.18`.
+
+## Installation
+
+```bash
+npm install --save @opentelemetry/exporter-logs-otlp-proto
+```
+
+## Further Documentation
+
+To see documentation and sample code for the traces exporter, as well as instructions for using TLS, see the [exporter-trace-otlp-proto package][trace-exporter-url].
+To see documentation and sample code for the metric exporter, see the [exporter-trace-otlp-proto package][metrics-exporter-url].
+
+## Example Setup
+
+```js
+const { LoggerProvider, SimpleLogRecordProcessor } = require('@opentelemetry/sdk-logs');
+const { OTLPLogsExporter } =  require('@opentelemetry/exporter-logs-otlp-proto');
+
+const collectorOptions = {
+  url: '<opentelemetry-collector-url>', // url is optional and can be omitted - default is http://localhost:4318/v1/logs
+  headers: {
+    foo: 'bar'
+  }, //an optional object containing custom headers to be sent with each request will only work with http
+};
+
+const logProvider = new LoggerProvider({resource: new Resource({'service.name': 'testApp'})});
+const logExporter = new OTLPLogsExporter(collectorOptions);
+logProvider.addLogRecordProcessor(new SimpleLogRecordProcessor(exporter));
+
+const logger = logProvider.getLogger('test_log_instrumentation');
+
+logger.emit({
+  //log data to emit
+})
+```
+
+## Exporter Timeout Configuration
+
+The OTLPLogsExporter has a timeout configuration option which is the maximum time, in milliseconds, the OTLP exporter will wait for each batch export. The default value is 10000ms.
+
+To override the default timeout duration, use the following options:
+
++ Set with environment variables:
+
+  | Environment variable         | Description |
+------------------------------|----------------------|-------------|
+  | OTEL_EXPORTER_OTLP_LOGS_TIMEOUT | The maximum waiting time, in milliseconds, allowed to send each OTLP trace batch. Default is 10000. |
+  | OTEL_EXPORTER_OTLP_TIMEOUT   | The maximum waiting time, in milliseconds, allowed to send each OTLP trace and metric batch. Default is 10000. |
+
+  > `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` takes precedence and overrides `OTEL_EXPORTER_OTLP_TIMEOUT`.
+
++ Provide `timeoutMillis` to OTLPLogsExporter with `collectorOptions`:
+
+  ```js
+  const collectorOptions = {
+    timeoutMillis: 15000,
+    url: '<opentelemetry-collector-url>', // url is optional and can be omitted - default is http://localhost:4318/v1/logs
+    headers: {
+      foo: 'bar'
+    }, //an optional object containing custom headers to be sent with each request will only work with http
+  };
+
+  const exporter = new OTLPLogsExporter(collectorOptions);
+  ```
+
+  > Providing `timeoutMillis` with `collectorOptions` takes precedence and overrides timeout set with environment variables.
+
+## OTLP Exporter Retry
+
+OTLP requires that transient errors be handled with a [retry strategy](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#retry).
+
+This retry policy has the following configuration, which there is currently no way to customize.
+
++ `DEFAULT_EXPORT_MAX_ATTEMPTS`: The maximum number of attempts, including the original request. Defaults to 5.
++ `DEFAULT_EXPORT_INITIAL_BACKOFF`: The initial backoff duration. Defaults to 1 second.
++ `DEFAULT_EXPORT_MAX_BACKOFF`: The maximum backoff duration. Defaults to 5 seconds.
++ `DEFAULT_EXPORT_BACKOFF_MULTIPLIER`: The backoff multiplier. Defaults to 1.5.
+
+This retry policy first checks if the response has a `'Retry-After'` header. If there is a `'Retry-After'` header, the exporter will wait the amount specified in the `'Retry-After'` header before retrying. If there is no `'Retry-After'` header, the exporter will use an exponential backoff with jitter retry strategy.
+
+  > The exporter will retry exporting within the [exporter timeout configuration](#Exporter-Timeout-Configuration) time.
+
+## Useful links
+
++ For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
++ For more about OpenTelemetry JavaScript: <https://github.com/open-telemetry/opentelemetry-js>
++ For help or feedback on this project, join us in [GitHub Discussions][discussions-url]
+
+## License
+
+Apache 2.0 - See [LICENSE][license-url] for more information.
+
+[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions
+[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/main/LICENSE
+[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat
+[npm-url]: https://www.npmjs.com/package/@opentelemetry/exporter-logs-otlp-proto
+[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fexporter-logs-otlp-proto.svg
+[opentelemetry-collector-url]: https://github.com/open-telemetry/opentelemetry-collector
+[semconv-resource-service-name]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#service
+[logs-exporter-url]: https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-logs-otlp-proto
+[trace-exporter-url]: https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/exporter-trace-otlp-proto
+[metrics-exporter-url]: https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-proto
diff --git a/experimental/packages/exporter-logs-otlp-proto/karma.conf.js b/experimental/packages/exporter-logs-otlp-proto/karma.conf.js
new file mode 100644
index 00000000000..4c60b54edba
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/karma.conf.js
@@ -0,0 +1,26 @@
+/*!
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const karmaWebpackConfig = require('../../../karma.webpack');
+const karmaBaseConfig = require('../../../karma.base');
+
+module.exports = (config) => {
+  config.set(Object.assign({}, karmaBaseConfig, {
+    webpack: karmaWebpackConfig,
+    files: ['test/browser/index-webpack.ts'],
+    preprocessors: { 'test/browser/index-webpack.ts': ['webpack'] }
+  }))
+};
diff --git a/experimental/packages/exporter-logs-otlp-proto/package.json b/experimental/packages/exporter-logs-otlp-proto/package.json
new file mode 100644
index 00000000000..016ec134867
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/package.json
@@ -0,0 +1,95 @@
+{
+  "name": "@opentelemetry/exporter-logs-otlp-proto",
+  "version": "0.39.1",
+  "description": "An OTLP exporter to send logs using protobuf over HTTP",
+  "main": "build/src/index.js",
+  "module": "build/esm/index.js",
+  "esnext": "build/esnext/index.js",
+  "types": "build/src/index.d.ts",
+  "repository": "open-telemetry/opentelemetry-js",
+  "browser": {
+    "./src/platform/index.ts": "./src/platform/browser/index.ts",
+    "./build/esm/platform/index.js": "./build/esm/platform/browser/index.js",
+    "./build/esnext/platform/index.js": "./build/esnext/platform/browser/index.js",
+    "./build/src/platform/index.js": "./build/src/platform/browser/index.js"
+  },
+  "scripts": {
+    "prepublishOnly": "npm run compile",
+    "compile": "tsc --build tsconfig.json tsconfig.esm.json tsconfig.esnext.json",
+    "clean": "tsc --build --clean tsconfig.json tsconfig.esm.json tsconfig.esnext.json",
+    "lint": "eslint . --ext .ts",
+    "lint:fix": "eslint . --ext .ts --fix",
+    "tdd": "npm run test -- --watch-extensions ts --watch",
+    "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts' --exclude 'test/browser/**/*.ts'",
+    "test:browser": "nyc karma start --single-run",
+    "version": "node ../../../scripts/version-update.js",
+    "watch": "tsc --build --watch tsconfig.json tsconfig.esm.json tsconfig.esnext.json",
+    "precompile": "lerna run version --scope $(npm pkg get name) --include-dependencies",
+    "prewatch": "npm run precompile",
+    "peer-api-check": "node ../../../scripts/peer-api-check.js",
+    "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../../"
+  },
+  "keywords": [
+    "opentelemetry",
+    "nodejs",
+    "protobuf",
+    "tracing",
+    "profiling",
+    "metrics",
+    "stats",
+    "logs"
+  ],
+  "author": "OpenTelemetry Authors",
+  "license": "Apache-2.0",
+  "engines": {
+    "node": ">=14"
+  },
+  "files": [
+    "build/esm/**/*.js",
+    "build/esm/**/*.js.map",
+    "build/esm/**/*.d.ts",
+    "build/esnext/**/*.js",
+    "build/esnext/**/*.js.map",
+    "build/esnext/**/*.d.ts",
+    "build/src/**/*.js",
+    "build/src/**/*.js.map",
+    "build/src/**/*.d.ts",
+    "build/protos/**/*.proto",
+    "doc",
+    "LICENSE",
+    "README.md"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "devDependencies": {
+    "@babel/core": "7.16.0",
+    "@opentelemetry/api": "1.4.1",
+    "@types/mocha": "10.0.0",
+    "@types/node": "18.6.5",
+    "@types/sinon": "10.0.13",
+    "codecov": "3.8.3",
+    "cpx": "1.5.0",
+    "mocha": "10.0.0",
+    "nyc": "15.1.0",
+    "sinon": "15.0.0",
+    "ts-loader": "8.4.0",
+    "ts-mocha": "10.0.0",
+    "typescript": "4.4.4"
+  },
+  "peerDependencies": {
+    "@opentelemetry/api": "^1.0.0"
+  },
+  "dependencies": {
+    "@opentelemetry/core": "1.13.0",
+    "@opentelemetry/otlp-transformer": "0.39.1",
+    "@opentelemetry/resources": "1.13.0",
+    "@opentelemetry/sdk-trace-base": "1.13.0",
+    "@opentelemetry/api-logs": "^0.39.1",
+    "@opentelemetry/otlp-exporter-base": "0.39.1",
+    "@opentelemetry/otlp-proto-exporter-base": "0.39.1",
+    "@opentelemetry/sdk-logs": "^0.39.1"
+  },
+  "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-logs-otlp-proto",
+  "sideEffects": false
+}
diff --git a/experimental/packages/exporter-logs-otlp-proto/src/index.ts b/experimental/packages/exporter-logs-otlp-proto/src/index.ts
new file mode 100644
index 00000000000..9fde4be45b2
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/src/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { OTLPLogsExporter } from './platform';
diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/OTLPLogsExporter.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/OTLPLogsExporter.ts
new file mode 100644
index 00000000000..185d5094f1a
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/OTLPLogsExporter.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { getEnv, baggageUtils } from '@opentelemetry/core';
+import {
+  OTLPExporterConfigBase,
+  appendResourcePathToUrl,
+  appendRootPathToUrlIfNeeded,
+} from '@opentelemetry/otlp-exporter-base';
+import {
+  OTLPProtoExporterBrowserBase,
+  ServiceClientType,
+} from '@opentelemetry/otlp-proto-exporter-base';
+import {
+  createExportLogsServiceRequest,
+  IExportLogsServiceRequest,
+} from '@opentelemetry/otlp-transformer';
+
+import { ReadableLogRecord, LogRecordExporter } from '@opentelemetry/sdk-logs';
+
+const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/logs';
+const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`;
+
+/**
+ * Collector Trace Exporter for Web
+ */
+export class OTLPLogsExporter
+  extends OTLPProtoExporterBrowserBase<
+    ReadableLogRecord,
+    IExportLogsServiceRequest
+  >
+  implements LogRecordExporter
+{
+  constructor(config: OTLPExporterConfigBase = {}) {
+    super(config);
+    this._headers = Object.assign(
+      this._headers,
+      baggageUtils.parseKeyPairsIntoRecord(
+        getEnv().OTEL_EXPORTER_OTLP_LOGS_HEADERS
+      )
+    );
+  }
+  convert(logs: ReadableLogRecord[]): IExportLogsServiceRequest {
+    return createExportLogsServiceRequest(logs);
+  }
+
+  getDefaultUrl(config: OTLPExporterConfigBase): string {
+    return typeof config.url === 'string'
+      ? config.url
+      : getEnv().OTEL_EXPORTER_OTLP_LOGS_ENDPOINT.length > 0
+      ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)
+      : getEnv().OTEL_EXPORTER_OTLP_ENDPOINT.length > 0
+      ? appendResourcePathToUrl(
+          getEnv().OTEL_EXPORTER_OTLP_ENDPOINT,
+          DEFAULT_COLLECTOR_RESOURCE_PATH
+        )
+      : DEFAULT_COLLECTOR_URL;
+  }
+
+  getServiceClientType() {
+    return ServiceClientType.LOGS;
+  }
+}
diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/index.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/index.ts
new file mode 100644
index 00000000000..e6968b34669
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { OTLPLogsExporter } from './OTLPLogsExporter';
diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/index.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/index.ts
new file mode 100644
index 00000000000..851ff9a015b
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { OTLPLogsExporter } from './node';
diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/node/OTLPLogsExporter.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/node/OTLPLogsExporter.ts
new file mode 100644
index 00000000000..88d97e21d3e
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/node/OTLPLogsExporter.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { getEnv, baggageUtils } from '@opentelemetry/core';
+import {
+  OTLPExporterConfigBase,
+  appendResourcePathToUrl,
+  appendRootPathToUrlIfNeeded,
+} from '@opentelemetry/otlp-exporter-base';
+import {
+  OTLPProtoExporterNodeBase,
+  ServiceClientType,
+} from '@opentelemetry/otlp-proto-exporter-base';
+import {
+  createExportLogsServiceRequest,
+  IExportLogsServiceRequest,
+} from '@opentelemetry/otlp-transformer';
+
+import { ReadableLogRecord, LogRecordExporter } from '@opentelemetry/sdk-logs';
+
+const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/logs';
+const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`;
+
+/**
+ * Collector Trace Exporter for Node
+ */
+export class OTLPLogsExporter
+  extends OTLPProtoExporterNodeBase<
+    ReadableLogRecord,
+    IExportLogsServiceRequest
+  >
+  implements LogRecordExporter
+{
+  constructor(config: OTLPExporterConfigBase = {}) {
+    super(config);
+    this.headers = Object.assign(
+      this.headers,
+      baggageUtils.parseKeyPairsIntoRecord(
+        getEnv().OTEL_EXPORTER_OTLP_LOGS_HEADERS
+      )
+    );
+  }
+  convert(logs: ReadableLogRecord[]): IExportLogsServiceRequest {
+    return createExportLogsServiceRequest(logs);
+  }
+
+  getDefaultUrl(config: OTLPExporterConfigBase): string {
+    return typeof config.url === 'string'
+      ? config.url
+      : getEnv().OTEL_EXPORTER_OTLP_LOGS_ENDPOINT.length > 0
+      ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)
+      : getEnv().OTEL_EXPORTER_OTLP_ENDPOINT.length > 0
+      ? appendResourcePathToUrl(
+          getEnv().OTEL_EXPORTER_OTLP_ENDPOINT,
+          DEFAULT_COLLECTOR_RESOURCE_PATH
+        )
+      : DEFAULT_COLLECTOR_URL;
+  }
+
+  getServiceClientType() {
+    return ServiceClientType.LOGS;
+  }
+}
diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/node/index.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/node/index.ts
new file mode 100644
index 00000000000..4797b306510
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/node/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { OTLPLogsExporter } from './OTLPLogsExporter';
diff --git a/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogsExporter.test.ts b/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogsExporter.test.ts
new file mode 100644
index 00000000000..382a132c3eb
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogsExporter.test.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as assert from 'assert';
+import * as sinon from 'sinon';
+import { OTLPLogsExporter } from '../../src/platform/browser/index';
+
+describe('OTLPLogsExporter - web', () => {
+  let collectorLogsExporter: OTLPLogsExporter;
+  describe('constructor', () => {
+    let onInitSpy: any;
+    beforeEach(() => {
+      onInitSpy = sinon.stub(OTLPLogsExporter.prototype, 'onInit');
+      const collectorExporterConfig = {
+        hostname: 'foo',
+        url: 'http://foo.bar.com',
+      };
+      collectorLogsExporter = new OTLPLogsExporter(collectorExporterConfig);
+    });
+    afterEach(() => {
+      sinon.restore();
+    });
+    it('should create an instance', () => {
+      assert.ok(typeof collectorLogsExporter !== 'undefined');
+    });
+    it('should call onInit', () => {
+      assert.strictEqual(onInitSpy.callCount, 1);
+    });
+    it('should set hostname', () => {
+      assert.strictEqual(collectorLogsExporter.hostname, 'foo');
+    });
+
+    it('should set url', () => {
+      assert.strictEqual(collectorLogsExporter.url, 'http://foo.bar.com');
+    });
+  });
+});
diff --git a/experimental/packages/exporter-logs-otlp-proto/test/browser/index-webpack.ts b/experimental/packages/exporter-logs-otlp-proto/test/browser/index-webpack.ts
new file mode 100644
index 00000000000..ae7d4b5a9dd
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/test/browser/index-webpack.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const testsContext = require.context('../browser', true, /test$/);
+testsContext.keys().forEach(testsContext);
+
+const srcContext = require.context('.', true, /src$/);
+srcContext.keys().forEach(srcContext);
diff --git a/experimental/packages/exporter-logs-otlp-proto/test/logHelper.ts b/experimental/packages/exporter-logs-otlp-proto/test/logHelper.ts
new file mode 100644
index 00000000000..7b8c8e8b9a0
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/test/logHelper.ts
@@ -0,0 +1,186 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { HrTime, TraceFlags } from '@opentelemetry/api';
+import { SeverityNumber } from '@opentelemetry/api-logs';
+import { Resource } from '@opentelemetry/resources';
+import * as assert from 'assert';
+import { VERSION } from '@opentelemetry/core';
+import {
+  IAnyValue,
+  IExportLogsServiceRequest,
+  IKeyValue,
+  ILogRecord,
+  IResource,
+} from '@opentelemetry/otlp-transformer';
+import { ReadableLogRecord } from '@opentelemetry/sdk-logs';
+import { Stream } from 'stream';
+
+export const mockedReadableLogRecord: ReadableLogRecord = {
+  resource: Resource.default().merge(
+    new Resource({
+      'resource-attribute': 'some resource-attr value',
+    })
+  ),
+  instrumentationScope: {
+    name: 'scope_name_1',
+    version: '0.1.0',
+    schemaUrl: 'http://url.to.schema',
+  },
+  hrTime: [1680253513, 123241635] as HrTime,
+  hrTimeObserved: [1680253513, 123241635] as HrTime,
+  attributes: {
+    'some-attribute': 'some attribute value',
+  },
+  severityNumber: SeverityNumber.ERROR,
+  severityText: 'error',
+  body: 'some_log_body',
+  spanContext: {
+    traceFlags: TraceFlags.SAMPLED,
+    traceId: '1f1008dc8e270e85c40a0d7c3939b278',
+    spanId: '5e107261f64fa53e',
+  },
+};
+export function ensureExportedAttributesAreCorrect(attributes: IKeyValue[]) {
+  assert.deepStrictEqual(
+    attributes,
+    [
+      {
+        key: 'some-attribute',
+        value: {
+          stringValue: 'some attribute value',
+        },
+      },
+    ],
+    'exported attributes are incorrect'
+  );
+}
+
+export function ensureExportedBodyIsCorrect(body?: IAnyValue) {
+  assert.deepStrictEqual(
+    body,
+    { stringValue: 'some_log_body' },
+    'exported attributes are incorrect'
+  );
+}
+
+export function ensureExportedLogRecordIsCorrect(logRecord: ILogRecord) {
+  ensureExportedBodyIsCorrect(logRecord.body);
+  ensureExportedAttributesAreCorrect(logRecord.attributes);
+  assert.strictEqual(
+    logRecord.timeUnixNano,
+    '1680253513123241728',
+    'timeUnixNano is wrong'
+  );
+  assert.strictEqual(
+    logRecord.observedTimeUnixNano,
+    '1680253513123241728',
+    'observedTimeUnixNano is wrong'
+  );
+  assert.strictEqual(
+    logRecord.severityNumber,
+    'SEVERITY_NUMBER_ERROR',
+    'severityNumber is wrong'
+  );
+  assert.strictEqual(logRecord.severityText, 'error', 'severityText is wrong');
+  assert.strictEqual(
+    logRecord.droppedAttributesCount,
+    0,
+    'droppedAttributesCount is wrong'
+  );
+  assert.strictEqual(logRecord.flags, TraceFlags.SAMPLED, 'flags is wrong');
+}
+
+export function ensureResourceIsCorrect(resource: IResource) {
+  assert.deepStrictEqual(resource, {
+    attributes: [
+      {
+        key: 'service.name',
+        value: {
+          stringValue: `unknown_service:${process.argv0}`,
+          value: 'stringValue',
+        },
+      },
+      {
+        key: 'telemetry.sdk.language',
+        value: {
+          stringValue: 'nodejs',
+          value: 'stringValue',
+        },
+      },
+      {
+        key: 'telemetry.sdk.name',
+        value: {
+          stringValue: 'opentelemetry',
+          value: 'stringValue',
+        },
+      },
+      {
+        key: 'telemetry.sdk.version',
+        value: {
+          stringValue: VERSION,
+          value: 'stringValue',
+        },
+      },
+      {
+        key: 'resource-attribute',
+        value: {
+          stringValue: 'some resource-attr value',
+          value: 'stringValue',
+        },
+      },
+    ],
+    droppedAttributesCount: 0,
+  });
+}
+
+export function ensureExportLogsServiceRequestIsSet(
+  json: IExportLogsServiceRequest
+) {
+  const resourceLogs = json.resourceLogs;
+  assert.strictEqual(resourceLogs?.length, 1, 'resourceLogs is missing');
+
+  const resource = resourceLogs?.[0].resource;
+  assert.ok(resource, 'resource is missing');
+
+  const scopeLogs = resourceLogs?.[0].scopeLogs;
+  assert.strictEqual(scopeLogs?.length, 1, 'scopeLogs is missing');
+
+  const scope = scopeLogs?.[0].scope;
+  assert.ok(scope, 'scope is missing');
+
+  const logRecords = scopeLogs?.[0].logRecords;
+  assert.strictEqual(logRecords?.length, 1, 'logs are missing');
+}
+
+export class MockedResponse extends Stream {
+  constructor(private _code: number, private _msg?: string) {
+    super();
+  }
+
+  send(data: string) {
+    this.emit('data', data);
+    this.emit('end');
+  }
+
+  get statusCode() {
+    return this._code;
+  }
+
+  get statusMessage() {
+    return this._msg;
+  }
+}
diff --git a/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogsExporter.test.ts b/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogsExporter.test.ts
new file mode 100644
index 00000000000..994a2f0ea47
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogsExporter.test.ts
@@ -0,0 +1,416 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { diag } from '@opentelemetry/api';
+import { ExportResultCode } from '@opentelemetry/core';
+import * as assert from 'assert';
+import * as http from 'http';
+import * as sinon from 'sinon';
+import { Stream, PassThrough } from 'stream';
+import * as zlib from 'zlib';
+import { OTLPLogsExporter } from '../../src';
+import {
+  ensureExportLogsServiceRequestIsSet,
+  ensureExportedLogRecordIsCorrect,
+  mockedReadableLogRecord,
+  MockedResponse,
+} from '../logHelper';
+import {
+  CompressionAlgorithm,
+  OTLPExporterNodeConfigBase,
+  OTLPExporterError,
+} from '@opentelemetry/otlp-exporter-base';
+import {
+  getExportRequestProto,
+  ServiceClientType,
+} from '@opentelemetry/otlp-proto-exporter-base';
+import { IExportLogsServiceRequest } from '@opentelemetry/otlp-transformer';
+import { ReadableLogRecord } from '@opentelemetry/sdk-logs';
+
+let fakeRequest: PassThrough;
+
+describe('OTLPLogsExporter - node with proto over http', () => {
+  let collectorExporter: OTLPLogsExporter;
+  let collectorExporterConfig: OTLPExporterNodeConfigBase;
+  let logs: ReadableLogRecord[];
+
+  afterEach(() => {
+    fakeRequest = new Stream.PassThrough();
+    sinon.restore();
+  });
+
+  describe('when configuring via environment', () => {
+    const envSource = process.env;
+    it('should use url defined in env that ends with root path and append version and signal path', () => {
+      envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/';
+      const collectorExporter = new OTLPLogsExporter();
+      assert.strictEqual(
+        collectorExporter.url,
+        `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}v1/logs`
+      );
+      envSource.OTEL_EXPORTER_OTLP_ENDPOINT = '';
+    });
+    it('should use url defined in env without checking if path is already present', () => {
+      envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/v1/logs';
+      const collectorExporter = new OTLPLogsExporter();
+      assert.strictEqual(
+        collectorExporter.url,
+        `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs`
+      );
+      envSource.OTEL_EXPORTER_OTLP_ENDPOINT = '';
+    });
+    it('should use url defined in env and append version and signal', () => {
+      envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar';
+      const collectorExporter = new OTLPLogsExporter();
+      assert.strictEqual(
+        collectorExporter.url,
+        `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs`
+      );
+      envSource.OTEL_EXPORTER_OTLP_ENDPOINT = '';
+    });
+    it('should override global exporter url with signal url defined in env', () => {
+      envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/';
+      envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.logs/';
+      const collectorExporter = new OTLPLogsExporter();
+      assert.strictEqual(
+        collectorExporter.url,
+        envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
+      );
+      envSource.OTEL_EXPORTER_OTLP_ENDPOINT = '';
+      envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = '';
+    });
+    it('should add root path when signal url defined in env contains no path and no root path', () => {
+      envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar';
+      const collectorExporter = new OTLPLogsExporter();
+      assert.strictEqual(
+        collectorExporter.url,
+        `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}/`
+      );
+      envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = '';
+    });
+    it('should not add root path when signal url defined in env contains root path but no path', () => {
+      envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar/';
+      const collectorExporter = new OTLPLogsExporter();
+      assert.strictEqual(
+        collectorExporter.url,
+        `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}`
+      );
+      envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = '';
+    });
+    it('should not add root path when signal url defined in env contains path', () => {
+      envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar/v1/logs';
+      const collectorExporter = new OTLPLogsExporter();
+      assert.strictEqual(
+        collectorExporter.url,
+        `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}`
+      );
+      envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = '';
+    });
+    it('should not add root path when signal url defined in env contains path and ends in /', () => {
+      envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar/v1/logs/';
+      const collectorExporter = new OTLPLogsExporter();
+      assert.strictEqual(
+        collectorExporter.url,
+        `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}`
+      );
+      envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = '';
+    });
+    it('should use headers defined via env', () => {
+      envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = 'foo=bar';
+      const collectorExporter = new OTLPLogsExporter();
+      assert.strictEqual(collectorExporter.headers.foo, 'bar');
+      envSource.OTEL_EXPORTER_OTLP_HEADERS = '';
+    });
+    it('should override global headers config with signal headers defined via env', () => {
+      envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo';
+      envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = 'foo=boo';
+      const collectorExporter = new OTLPLogsExporter();
+      assert.strictEqual(collectorExporter.headers.foo, 'boo');
+      assert.strictEqual(collectorExporter.headers.bar, 'foo');
+      envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = '';
+      envSource.OTEL_EXPORTER_OTLP_HEADERS = '';
+    });
+  });
+
+  describe('export', () => {
+    beforeEach(() => {
+      collectorExporterConfig = {
+        headers: {
+          foo: 'bar',
+        },
+        hostname: 'foo',
+        url: 'http://foo.bar.com',
+        keepAlive: true,
+        httpAgentOptions: { keepAliveMsecs: 2000 },
+      };
+      collectorExporter = new OTLPLogsExporter(collectorExporterConfig);
+      logs = [];
+      logs.push(Object.assign({}, mockedReadableLogRecord));
+    });
+    afterEach(() => {
+      sinon.restore();
+    });
+
+    it('should open the connection', done => {
+      collectorExporter.export(logs, () => {});
+
+      sinon.stub(http, 'request').callsFake((options: any, cb: any) => {
+        assert.strictEqual(options.hostname, 'foo.bar.com');
+        assert.strictEqual(options.method, 'POST');
+        assert.strictEqual(options.path, '/');
+
+        const mockRes = new MockedResponse(200);
+        cb(mockRes);
+        mockRes.send('success');
+        done();
+        return fakeRequest as any;
+      });
+    });
+
+    it('should set custom headers', done => {
+      collectorExporter.export(logs, () => {});
+
+      sinon.stub(http, 'request').callsFake((options: any, cb: any) => {
+        assert.strictEqual(options.headers['foo'], 'bar');
+
+        const mockRes = new MockedResponse(200);
+        cb(mockRes);
+        mockRes.send('success');
+        done();
+        return fakeRequest as any;
+      });
+    });
+
+    it('should have keep alive and keepAliveMsecs option set', done => {
+      collectorExporter.export(logs, () => {});
+
+      sinon.stub(http, 'request').callsFake((options: any, cb: any) => {
+        assert.strictEqual(options.agent.keepAlive, true);
+        assert.strictEqual(options.agent.options.keepAliveMsecs, 2000);
+
+        const mockRes = new MockedResponse(200);
+        cb(mockRes);
+        mockRes.send('success');
+        done();
+        return fakeRequest as any;
+      });
+    });
+
+    it('should successfully send the logs', done => {
+      const fakeRequest = new Stream.PassThrough();
+      sinon.stub(http, 'request').returns(fakeRequest as any);
+
+      let buff = Buffer.from('');
+      fakeRequest.on('end', () => {
+        const ExportLogsServiceRequestProto = getExportRequestProto(
+          ServiceClientType.LOGS
+        );
+        const data = ExportLogsServiceRequestProto.decode(buff);
+        const json = data?.toJSON() as IExportLogsServiceRequest;
+        const log1 = json.resourceLogs?.[0].scopeLogs?.[0].logRecords?.[0];
+        assert.ok(typeof log1 !== 'undefined', "log doesn't exist");
+        ensureExportedLogRecordIsCorrect(log1);
+
+        ensureExportLogsServiceRequestIsSet(json);
+
+        done();
+      });
+
+      fakeRequest.on('data', chunk => {
+        buff = Buffer.concat([buff, chunk]);
+      });
+
+      const clock = sinon.useFakeTimers();
+      collectorExporter.export(logs, () => {});
+      clock.tick(200);
+      clock.restore();
+    });
+
+    it('should log the successful message', done => {
+      // Need to stub/spy on the underlying logger as the "diag" instance is global
+      const spyLoggerError = sinon.stub(diag, 'error');
+
+      collectorExporter.export(logs, result => {
+        assert.strictEqual(result.code, ExportResultCode.SUCCESS);
+        assert.strictEqual(spyLoggerError.args.length, 0);
+        done();
+      });
+
+      sinon.stub(http, 'request').callsFake((options: any, cb: any) => {
+        const mockRes = new MockedResponse(200);
+        cb(mockRes);
+        mockRes.send('success');
+        return fakeRequest as any;
+      });
+    });
+
+    it('should log the error message', done => {
+      collectorExporter.export(logs, result => {
+        assert.strictEqual(result.code, ExportResultCode.FAILED);
+        // @ts-expect-error verify error code
+        assert.strictEqual(result.error.code, 400);
+        done();
+      });
+
+      sinon.stub(http, 'request').callsFake((options: any, cb: any) => {
+        const mockResError = new MockedResponse(400);
+        cb(mockResError);
+        mockResError.send('failed');
+
+        return fakeRequest as any;
+      });
+    });
+  });
+  describe('export - with compression', () => {
+    beforeEach(() => {
+      collectorExporterConfig = {
+        headers: {
+          foo: 'bar',
+        },
+        hostname: 'foo',
+        url: 'http://foo.bar.com',
+        keepAlive: true,
+        compression: CompressionAlgorithm.GZIP,
+        httpAgentOptions: { keepAliveMsecs: 2000 },
+      };
+      collectorExporter = new OTLPLogsExporter(collectorExporterConfig);
+      logs = [];
+      logs.push(Object.assign({}, mockedReadableLogRecord));
+    });
+    afterEach(() => {
+      sinon.restore();
+    });
+
+    it('should successfully send the logs', done => {
+      const fakeRequest = new Stream.PassThrough();
+      sinon.stub(http, 'request').returns(fakeRequest as any);
+      const spySetHeader = sinon.spy();
+      (fakeRequest as any).setHeader = spySetHeader;
+
+      let buff = Buffer.from('');
+      fakeRequest.on('end', () => {
+        const unzippedBuff = zlib.gunzipSync(buff);
+        const ExportLogsServiceRequestProto = getExportRequestProto(
+          ServiceClientType.LOGS
+        );
+        const data = ExportLogsServiceRequestProto.decode(unzippedBuff);
+        const json = data?.toJSON() as IExportLogsServiceRequest;
+        const log1 = json.resourceLogs?.[0].scopeLogs?.[0].logRecords?.[0];
+        assert.ok(typeof log1 !== 'undefined', "log doesn't exist");
+        ensureExportedLogRecordIsCorrect(log1);
+
+        ensureExportLogsServiceRequestIsSet(json);
+        assert.ok(spySetHeader.calledWith('Content-Encoding', 'gzip'));
+
+        done();
+      });
+
+      fakeRequest.on('data', chunk => {
+        buff = Buffer.concat([buff, chunk]);
+      });
+
+      const clock = sinon.useFakeTimers();
+      collectorExporter.export(logs, () => {});
+      clock.tick(200);
+      clock.restore();
+    });
+  });
+});
+
+describe('export - real http request destroyed before response received', () => {
+  let collectorExporter: OTLPLogsExporter;
+  let collectorExporterConfig: OTLPExporterNodeConfigBase;
+  let logs: ReadableLogRecord[];
+  const server = http.createServer((_, res) => {
+    setTimeout(() => {
+      res.statusCode = 200;
+      res.end();
+    }, 200);
+  });
+  before(done => {
+    server.listen(8082, done);
+  });
+  after(done => {
+    server.close(done);
+  });
+  it('should log the timeout request error message when timeout is 1', done => {
+    collectorExporterConfig = {
+      url: 'http://localhost:8082',
+      timeoutMillis: 1,
+    };
+    collectorExporter = new OTLPLogsExporter(collectorExporterConfig);
+    logs = [];
+    logs.push(Object.assign({}, mockedReadableLogRecord));
+
+    collectorExporter.export(logs, result => {
+      assert.strictEqual(result.code, ExportResultCode.FAILED);
+      const error = result.error as OTLPExporterError;
+      assert.ok(error !== undefined);
+      assert.strictEqual(error.message, 'Request Timeout');
+      done();
+    });
+  });
+  it('should log the timeout request error message when timeout is 100', done => {
+    collectorExporterConfig = {
+      url: 'http://localhost:8082',
+      timeoutMillis: 100,
+    };
+    collectorExporter = new OTLPLogsExporter(collectorExporterConfig);
+    logs = [];
+    logs.push(Object.assign({}, mockedReadableLogRecord));
+
+    collectorExporter.export(logs, result => {
+      assert.strictEqual(result.code, ExportResultCode.FAILED);
+      const error = result.error as OTLPExporterError;
+      assert.ok(error !== undefined);
+      assert.strictEqual(error.message, 'Request Timeout');
+      done();
+    });
+  });
+});
+
+describe('export - real http request destroyed after response received', () => {
+  let collectorExporter: OTLPLogsExporter;
+  let collectorExporterConfig: OTLPExporterNodeConfigBase;
+  let logs: ReadableLogRecord[];
+
+  const server = http.createServer((_, res) => {
+    res.write('writing something');
+  });
+  before(done => {
+    server.listen(8082, done);
+  });
+  after(done => {
+    server.close(done);
+  });
+  it('should log the timeout request error message', done => {
+    collectorExporterConfig = {
+      url: 'http://localhost:8082',
+      timeoutMillis: 300,
+    };
+    collectorExporter = new OTLPLogsExporter(collectorExporterConfig);
+    logs = [];
+    logs.push(Object.assign({}, mockedReadableLogRecord));
+
+    collectorExporter.export(logs, result => {
+      assert.strictEqual(result.code, ExportResultCode.FAILED);
+      const error = result.error as OTLPExporterError;
+      assert.ok(error !== undefined);
+      assert.strictEqual(error.message, 'Request Timeout');
+      done();
+    });
+  });
+});
diff --git a/experimental/packages/exporter-logs-otlp-proto/tsconfig.esm.json b/experimental/packages/exporter-logs-otlp-proto/tsconfig.esm.json
new file mode 100644
index 00000000000..5eba4694be3
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/tsconfig.esm.json
@@ -0,0 +1,40 @@
+{
+  "extends": "../../../tsconfig.base.esm.json",
+  "compilerOptions": {
+    "outDir": "build/esm",
+    "rootDir": "src",
+    "tsBuildInfoFile": "build/esm/tsconfig.esm.tsbuildinfo"
+  },
+  "include": [
+    "src/**/*.ts"
+  ],
+  "references": [
+    {
+      "path": "../../../api"
+    },
+    {
+      "path": "../../../packages/opentelemetry-core"
+    },
+    {
+      "path": "../../../packages/opentelemetry-resources"
+    },
+    {
+      "path": "../../../packages/opentelemetry-sdk-trace-base"
+    },
+    {
+      "path": "../api-logs"
+    },
+    {
+      "path": "../otlp-exporter-base"
+    },
+    {
+      "path": "../otlp-proto-exporter-base"
+    },
+    {
+      "path": "../otlp-transformer"
+    },
+    {
+      "path": "../sdk-logs"
+    }
+  ]
+}
diff --git a/experimental/packages/exporter-logs-otlp-proto/tsconfig.esnext.json b/experimental/packages/exporter-logs-otlp-proto/tsconfig.esnext.json
new file mode 100644
index 00000000000..b37520a70f3
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/tsconfig.esnext.json
@@ -0,0 +1,40 @@
+{
+  "extends": "../../../tsconfig.base.esnext.json",
+  "compilerOptions": {
+    "outDir": "build/esnext",
+    "rootDir": "src",
+    "tsBuildInfoFile": "build/esnext/tsconfig.esnext.tsbuildinfo"
+  },
+  "include": [
+    "src/**/*.ts"
+  ],
+  "references": [
+    {
+      "path": "../../../api"
+    },
+    {
+      "path": "../../../packages/opentelemetry-core"
+    },
+    {
+      "path": "../../../packages/opentelemetry-resources"
+    },
+    {
+      "path": "../../../packages/opentelemetry-sdk-trace-base"
+    },
+    {
+      "path": "../api-logs"
+    },
+    {
+      "path": "../otlp-exporter-base"
+    },
+    {
+      "path": "../otlp-proto-exporter-base"
+    },
+    {
+      "path": "../otlp-transformer"
+    },
+    {
+      "path": "../sdk-logs"
+    }
+  ]
+}
diff --git a/experimental/packages/exporter-logs-otlp-proto/tsconfig.json b/experimental/packages/exporter-logs-otlp-proto/tsconfig.json
new file mode 100644
index 00000000000..2349f8706c7
--- /dev/null
+++ b/experimental/packages/exporter-logs-otlp-proto/tsconfig.json
@@ -0,0 +1,40 @@
+{
+  "extends": "../../../tsconfig.base.json",
+  "compilerOptions": {
+    "outDir": "build",
+    "rootDir": "."
+  },
+  "include": [
+    "src/**/*.ts",
+    "test/**/*.ts"
+  ],
+  "references": [
+    {
+      "path": "../../../api"
+    },
+    {
+      "path": "../../../packages/opentelemetry-core"
+    },
+    {
+      "path": "../../../packages/opentelemetry-resources"
+    },
+    {
+      "path": "../../../packages/opentelemetry-sdk-trace-base"
+    },
+    {
+      "path": "../api-logs"
+    },
+    {
+      "path": "../otlp-exporter-base"
+    },
+    {
+      "path": "../otlp-proto-exporter-base"
+    },
+    {
+      "path": "../otlp-transformer"
+    },
+    {
+      "path": "../sdk-logs"
+    }
+  ]
+}
diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts
index 4e9f95d5c0d..34c80d6e82a 100644
--- a/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts
+++ b/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts
@@ -22,13 +22,8 @@ import {
   OTLPExporterConfigBase,
   sendWithXhr,
 } from '@opentelemetry/otlp-exporter-base';
-import * as root from '../../generated/root';
 
-interface ExportRequestType<T, R = T & { toJSON: () => unknown }> {
-  create(properties?: T): R;
-  encode(message: T, writer?: protobuf.Writer): protobuf.Writer;
-  decode(reader: protobuf.Reader | Uint8Array, length?: number): R;
-}
+import { getExportRequestProto } from '../util';
 
 /**
  * Collector Exporter abstract base class
@@ -41,18 +36,6 @@ export abstract class OTLPProtoExporterBrowserBase<
     super(config);
   }
 
-  private _getExportRequestProto(
-    clientType: ServiceClientType
-  ): ExportRequestType<ServiceRequest> {
-    if (clientType === ServiceClientType.SPANS) {
-      // eslint-disable-next-line
-      return root.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest as unknown as ExportRequestType<ServiceRequest>;
-    } else {
-      // eslint-disable-next-line
-      return root.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest as unknown as ExportRequestType<ServiceRequest>;
-    }
-  }
-
   override send(
     objects: ExportItem[],
     onSuccess: () => void,
@@ -64,7 +47,7 @@ export abstract class OTLPProtoExporterBrowserBase<
     }
 
     const serviceRequest = this.convert(objects);
-    const exportRequestType = this._getExportRequestProto(
+    const exportRequestType = getExportRequestProto(
       this.getServiceClientType()
     );
     const message = exportRequestType.create(serviceRequest);
diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/index.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/index.ts
index 22efeb03093..fc344756ea5 100644
--- a/experimental/packages/otlp-proto-exporter-base/src/platform/index.ts
+++ b/experimental/packages/otlp-proto-exporter-base/src/platform/index.ts
@@ -14,11 +14,7 @@
  * limitations under the License.
  */
 
-export {
-  OTLPProtoExporterNodeBase,
-  ExportRequestType,
-  getExportRequestProto,
-  send,
-} from './node';
+export { OTLPProtoExporterNodeBase, send } from './node';
 export { OTLPProtoExporterBrowserBase } from './browser';
 export { ServiceClientType } from './types';
+export { ExportRequestType, getExportRequestProto } from './util';
diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/node/index.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/node/index.ts
index c9c84bdac3f..08016fec935 100644
--- a/experimental/packages/otlp-proto-exporter-base/src/platform/node/index.ts
+++ b/experimental/packages/otlp-proto-exporter-base/src/platform/node/index.ts
@@ -15,4 +15,4 @@
  */
 
 export { OTLPProtoExporterNodeBase } from './OTLPProtoExporterNodeBase';
-export { ExportRequestType, getExportRequestProto, send } from './util';
+export { send } from './util';
diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/node/util.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/node/util.ts
index 5cdbfa396d1..36fe866d678 100644
--- a/experimental/packages/otlp-proto-exporter-base/src/platform/node/util.ts
+++ b/experimental/packages/otlp-proto-exporter-base/src/platform/node/util.ts
@@ -14,33 +14,14 @@
  * limitations under the License.
  */
 
-import { ServiceClientType } from '../types';
 import { OTLPProtoExporterNodeBase } from './OTLPProtoExporterNodeBase';
 import {
   CompressionAlgorithm,
   OTLPExporterError,
   sendWithHttp,
 } from '@opentelemetry/otlp-exporter-base';
-import type * as protobuf from 'protobufjs';
-import * as root from '../../generated/root';
 
-export interface ExportRequestType<T, R = T & { toJSON: () => unknown }> {
-  create(properties?: T): R;
-  encode(message: T, writer?: protobuf.Writer): protobuf.Writer;
-  decode(reader: protobuf.Reader | Uint8Array, length?: number): R;
-}
-
-export function getExportRequestProto<ServiceRequest>(
-  clientType: ServiceClientType
-): ExportRequestType<ServiceRequest> {
-  if (clientType === ServiceClientType.SPANS) {
-    return root.opentelemetry.proto.collector.trace.v1
-      .ExportTraceServiceRequest as unknown as ExportRequestType<ServiceRequest>;
-  } else {
-    return root.opentelemetry.proto.collector.metrics.v1
-      .ExportMetricsServiceRequest as unknown as ExportRequestType<ServiceRequest>;
-  }
-}
+import { getExportRequestProto } from '../util';
 
 export function send<ExportItem, ServiceRequest>(
   collector: OTLPProtoExporterNodeBase<ExportItem, ServiceRequest>,
diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/types.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/types.ts
index 389cfb1f522..b15ad36abc9 100644
--- a/experimental/packages/otlp-proto-exporter-base/src/platform/types.ts
+++ b/experimental/packages/otlp-proto-exporter-base/src/platform/types.ts
@@ -17,4 +17,5 @@
 export enum ServiceClientType {
   SPANS,
   METRICS,
+  LOGS,
 }
diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/util.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/util.ts
new file mode 100644
index 00000000000..c6e1272816f
--- /dev/null
+++ b/experimental/packages/otlp-proto-exporter-base/src/platform/util.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as root from '../generated/root';
+import { ServiceClientType } from './types';
+
+export interface ExportRequestType<T, R = T & { toJSON: () => unknown }> {
+  create(properties?: T): R;
+  encode(message: T, writer?: protobuf.Writer): protobuf.Writer;
+  decode(reader: protobuf.Reader | Uint8Array, length?: number): R;
+}
+
+export function getExportRequestProto<ServiceRequest>(
+  clientType: ServiceClientType
+): ExportRequestType<ServiceRequest> {
+  if (clientType === ServiceClientType.SPANS) {
+    return root.opentelemetry.proto.collector.trace.v1
+      .ExportTraceServiceRequest as unknown as ExportRequestType<ServiceRequest>;
+  } else if (clientType === ServiceClientType.LOGS) {
+    return root.opentelemetry.proto.collector.logs.v1
+      .ExportLogsServiceRequest as unknown as ExportRequestType<ServiceRequest>;
+  } else {
+    return root.opentelemetry.proto.collector.metrics.v1
+      .ExportMetricsServiceRequest as unknown as ExportRequestType<ServiceRequest>;
+  }
+}
diff --git a/tsconfig.esm.json b/tsconfig.esm.json
index e10c9262147..2fec210b9af 100644
--- a/tsconfig.esm.json
+++ b/tsconfig.esm.json
@@ -14,6 +14,9 @@
     {
       "path": "experimental/packages/exporter-logs-otlp-http/tsconfig.esm.json"
     },
+    {
+      "path": "experimental/packages/exporter-logs-otlp-proto/tsconfig.esm.json"
+    },
     {
       "path": "experimental/packages/exporter-trace-otlp-http/tsconfig.esm.json"
     },
diff --git a/tsconfig.esnext.json b/tsconfig.esnext.json
index 507c4d4309b..c5970c9c5c2 100644
--- a/tsconfig.esnext.json
+++ b/tsconfig.esnext.json
@@ -14,6 +14,9 @@
     {
       "path": "experimental/packages/exporter-logs-otlp-http/tsconfig.esnext.json"
     },
+    {
+      "path": "experimental/packages/exporter-logs-otlp-proto/tsconfig.esnext.json"
+    },
     {
       "path": "experimental/packages/exporter-trace-otlp-http/tsconfig.esnext.json"
     },
diff --git a/tsconfig.json b/tsconfig.json
index 7e90e75d2d2..acf73290872 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,6 +9,7 @@
       "experimental/packages/api-logs",
       "experimental/packages/exporter-logs-otlp-grpc",
       "experimental/packages/exporter-logs-otlp-http",
+      "experimental/packages/exporter-logs-otlp-proto",
       "experimental/packages/exporter-trace-otlp-grpc",
       "experimental/packages/exporter-trace-otlp-http",
       "experimental/packages/exporter-trace-otlp-proto",
@@ -71,6 +72,9 @@
     {
       "path": "experimental/packages/exporter-logs-otlp-http"
     },
+    {
+      "path": "experimental/packages/exporter-logs-otlp-proto"
+    },
     {
       "path": "experimental/packages/exporter-trace-otlp-grpc"
     },