From 3c8a5076fcc81d79200c65e17f48aab2c11db05b Mon Sep 17 00:00:00 2001
From: Mike Goldsmith <goldsmith.mike@gmail.com>
Date: Wed, 18 Sep 2024 23:45:29 +0100
Subject: [PATCH] Add Azure Resource Logs translator (#34830)

**Description**:

This PR adds a new Azure Resource Logs translator that can be used to
convert Azure events into resource logs events using Semantic
conventions defined here
[here](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/azure/events.md).

This supersedes following draft PR by updating it to have the latest
semconv:
- #32486

*Note*: A follow-up PR will update the Azure receiver to use the new
translator.

**Testing**:

Includes unit tests to verify expected behaviours and data structures.

**Documentation**:

---

cc @markrendle @lmolkova @TylerHelmuth

---------

Signed-off-by: Alex Boten <223565+codeboten@users.noreply.github.com>
Co-authored-by: Mark Rendle <mark@rendlelabs.com>
Co-authored-by: Alex Boten <223565+codeboten@users.noreply.github.com>
Co-authored-by: Curtis Robert <crobert@splunk.com>
Co-authored-by: Tyler Helmuth <12352919+TylerHelmuth@users.noreply.github.com>
---
 .chloggen/azureeventhub_otelsc.yaml           |  27 +
 .github/CODEOWNERS                            |   1 +
 .github/ISSUE_TEMPLATE/bug_report.yaml        |   1 +
 .github/ISSUE_TEMPLATE/feature_request.yaml   |   1 +
 .github/ISSUE_TEMPLATE/other.yaml             |   1 +
 .github/ISSUE_TEMPLATE/unmaintained.yaml      |   1 +
 pkg/translator/azurelogs/Makefile             |   1 +
 .../azurelogs/complex_conversions.go          | 105 +++
 .../azurelogs/complex_conversions_test.go     |  76 ++
 pkg/translator/azurelogs/go.mod               |  44 ++
 pkg/translator/azurelogs/go.sum               | 103 +++
 pkg/translator/azurelogs/metadata.yaml        |   3 +
 pkg/translator/azurelogs/normalize.go         |  77 ++
 pkg/translator/azurelogs/package_test.go      |  14 +
 pkg/translator/azurelogs/property_names.go    | 298 +++++++
 .../azurelogs/resourcelogs_to_logs.go         | 256 ++++++
 .../azurelogs/resourcelogs_to_logs_test.go    | 742 ++++++++++++++++++
 .../testdata/log-appserviceapplogs.json       |  18 +
 .../testdata/log-appserviceauditlogs.json     |  16 +
 .../testdata/log-appserviceconsolelogs.json   |  14 +
 .../testdata/log-appservicehttplogs.json      |  34 +
 .../log-appserviceipsecauditlogs.json         |  18 +
 .../testdata/log-appserviceplatformlogs.json  |  21 +
 .../testdata/log-azurecdnaccesslog.json       |  34 +
 .../azurelogs/testdata/log-bad-level.json     |  39 +
 .../azurelogs/testdata/log-bad-time.json      |  45 ++
 .../testdata/log-frontdooraccesslog.json      |  46 ++
 .../testdata/log-frontdoorhealthprobelog.json |  22 +
 .../testdata/log-frontdoorwaflog.json         |  18 +
 .../azurelogs/testdata/log-maximum.json       |  85 ++
 .../azurelogs/testdata/log-minimum-2.json     |  16 +
 .../azurelogs/testdata/log-minimum.json       |  10 +
 versions.yaml                                 |   1 +
 33 files changed, 2188 insertions(+)
 create mode 100644 .chloggen/azureeventhub_otelsc.yaml
 create mode 100644 pkg/translator/azurelogs/Makefile
 create mode 100644 pkg/translator/azurelogs/complex_conversions.go
 create mode 100644 pkg/translator/azurelogs/complex_conversions_test.go
 create mode 100644 pkg/translator/azurelogs/go.mod
 create mode 100644 pkg/translator/azurelogs/go.sum
 create mode 100644 pkg/translator/azurelogs/metadata.yaml
 create mode 100644 pkg/translator/azurelogs/normalize.go
 create mode 100644 pkg/translator/azurelogs/package_test.go
 create mode 100644 pkg/translator/azurelogs/property_names.go
 create mode 100644 pkg/translator/azurelogs/resourcelogs_to_logs.go
 create mode 100644 pkg/translator/azurelogs/resourcelogs_to_logs_test.go
 create mode 100644 pkg/translator/azurelogs/testdata/log-appserviceapplogs.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-appserviceauditlogs.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-appserviceconsolelogs.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-appservicehttplogs.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-appserviceipsecauditlogs.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-appserviceplatformlogs.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-azurecdnaccesslog.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-bad-level.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-bad-time.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-frontdooraccesslog.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-frontdoorhealthprobelog.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-frontdoorwaflog.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-maximum.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-minimum-2.json
 create mode 100644 pkg/translator/azurelogs/testdata/log-minimum.json

diff --git a/.chloggen/azureeventhub_otelsc.yaml b/.chloggen/azureeventhub_otelsc.yaml
new file mode 100644
index 000000000000..02cd499d507d
--- /dev/null
+++ b/.chloggen/azureeventhub_otelsc.yaml
@@ -0,0 +1,27 @@
+# Use this changelog template to create an entry for release notes.
+
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: new_component
+
+# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
+component: azurelogs_translater
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Adds a new translater that converts Azure EventHub logs to OpenTelemetry logs used by the Azure Events Hub receiver.
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [39704]
+
+# (Optional) One or more lines of additional information to render under the primary note.
+# These lines will be padded with 2 spaces and then inserted directly into the document.
+# Use pipe (|) for multiline entries.
+subtext:
+
+# If your change doesn't affect end users or the exported elements of any package,
+# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
+# Optional: The change log or logs in which this entry should be included.
+# e.g. '[user]' or '[user, api]'
+# Include 'user' if the change is relevant to end users.
+# Include 'api' if there is a change to a library API.
+# Default: '[user]'
+change_logs: [user]
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 56dab2a7b0ae..d6ac80f48e77 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -158,6 +158,7 @@ pkg/sampling/                                                       @open-teleme
 pkg/stanza/                                                         @open-telemetry/collector-contrib-approvers @djaglowski
 pkg/stanza/fileconsumer/                                            @open-telemetry/collector-contrib-approvers @djaglowski
 pkg/translator/azure/                                               @open-telemetry/collector-contrib-approvers @open-telemetry/collector-approvers @atoulme @cparkins
+pkg/translator/azurelogs/                                           @open-telemetry/collector-contrib-approvers @atoulme @cparkins @MikeGoldsmith
 pkg/translator/jaeger/                                              @open-telemetry/collector-contrib-approvers @open-telemetry/collector-approvers @frzifus
 pkg/translator/loki/                                                @open-telemetry/collector-contrib-approvers @gouthamve @jpkrohling @mar4uk
 pkg/translator/opencensus/                                          @open-telemetry/collector-contrib-approvers @open-telemetry/collector-approvers
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 1ec802e83d93..95ccb52f49bd 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -154,6 +154,7 @@ body:
       - pkg/stanza
       - pkg/stanza/fileconsumer
       - pkg/translator/azure
+      - pkg/translator/azurelogs
       - pkg/translator/jaeger
       - pkg/translator/loki
       - pkg/translator/opencensus
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index 3027e60c72a8..3ca09acbec30 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -148,6 +148,7 @@ body:
       - pkg/stanza
       - pkg/stanza/fileconsumer
       - pkg/translator/azure
+      - pkg/translator/azurelogs
       - pkg/translator/jaeger
       - pkg/translator/loki
       - pkg/translator/opencensus
diff --git a/.github/ISSUE_TEMPLATE/other.yaml b/.github/ISSUE_TEMPLATE/other.yaml
index e1e3cc57cd59..e0fe9c234f1e 100644
--- a/.github/ISSUE_TEMPLATE/other.yaml
+++ b/.github/ISSUE_TEMPLATE/other.yaml
@@ -148,6 +148,7 @@ body:
       - pkg/stanza
       - pkg/stanza/fileconsumer
       - pkg/translator/azure
+      - pkg/translator/azurelogs
       - pkg/translator/jaeger
       - pkg/translator/loki
       - pkg/translator/opencensus
diff --git a/.github/ISSUE_TEMPLATE/unmaintained.yaml b/.github/ISSUE_TEMPLATE/unmaintained.yaml
index 9f8c4ca17bab..95278e3a6514 100644
--- a/.github/ISSUE_TEMPLATE/unmaintained.yaml
+++ b/.github/ISSUE_TEMPLATE/unmaintained.yaml
@@ -153,6 +153,7 @@ body:
       - pkg/stanza
       - pkg/stanza/fileconsumer
       - pkg/translator/azure
+      - pkg/translator/azurelogs
       - pkg/translator/jaeger
       - pkg/translator/loki
       - pkg/translator/opencensus
diff --git a/pkg/translator/azurelogs/Makefile b/pkg/translator/azurelogs/Makefile
new file mode 100644
index 000000000000..bdd863a203be
--- /dev/null
+++ b/pkg/translator/azurelogs/Makefile
@@ -0,0 +1 @@
+include ../../../Makefile.Common
diff --git a/pkg/translator/azurelogs/complex_conversions.go b/pkg/translator/azurelogs/complex_conversions.go
new file mode 100644
index 000000000000..56232f3c07a4
--- /dev/null
+++ b/pkg/translator/azurelogs/complex_conversions.go
@@ -0,0 +1,105 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package azurelogs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azurelogs"
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+type ComplexConversion func(string, any, map[string]any) bool
+type TypeConversion func(string, any, map[string]any, string) bool
+
+var conversions = map[string]ComplexConversion{
+	"AzureCdnAccessLog:SecurityProtocol":               azureCdnAccessLogSecurityProtocol,
+	"FrontDoorAccessLog:securityProtocol":              azureCdnAccessLogSecurityProtocol,
+	"AppServiceHTTPLogs:Protocol":                      appServiceHTTPLogsProtocol,
+	"AppServiceHTTPLogs:TimeTaken":                     appServiceHTTPLogTimeTakenMilliseconds,
+	"FrontDoorHealthProbeLog:DNSLatencyMicroseconds":   frontDoorHealthProbeLogDNSLatencyMicroseconds,
+	"FrontDoorHealthProbeLog:totalLatencyMilliseconds": frontDoorHealthProbeLogTotalLatencyMilliseconds,
+}
+
+// Splits the "TLS 1.2" value into "TLS" and "1.2" and sets as "network.protocol.name" and "network.protocol.version"
+func azureCdnAccessLogSecurityProtocol(_ string, value any, attrs map[string]any) bool {
+	if str, ok := value.(string); ok {
+		if parts := strings.SplitN(str, " ", 2); len(parts) == 2 {
+			attrs["tls.protocol.name"] = strings.ToLower(parts[0])
+			attrs["tls.protocol.version"] = parts[1]
+			return true
+		}
+	}
+	return false
+}
+
+// Splits the "HTTP/1.1" value into "HTTP" and "1.1" and sets as "network.protocol.name" and "network.protocol.version"
+func appServiceHTTPLogsProtocol(_ string, value any, attrs map[string]any) bool {
+	if str, ok := value.(string); ok {
+		if parts := strings.SplitN(str, "/", 2); len(parts) == 2 {
+			attrs["network.protocol.name"] = strings.ToLower(parts[0])
+			attrs["network.protocol.version"] = parts[1]
+			return true
+		}
+	}
+	return false
+}
+
+// Converts Microseconds value to Seconds and sets as "dns.lookup.duration"
+func frontDoorHealthProbeLogDNSLatencyMicroseconds(_ string, value any, attrs map[string]any) bool {
+	microseconds, ok := tryParseFloat64(value)
+	if !ok {
+		return false
+	}
+	seconds := microseconds / 1_000_000
+	attrs["dns.lookup.duration"] = seconds
+	return true
+}
+
+// Converts Milliseconds value to Seconds and sets as "http.client.request.duration"
+func frontDoorHealthProbeLogTotalLatencyMilliseconds(_ string, value any, attrs map[string]any) bool {
+	milliseconds, ok := tryParseFloat64(value)
+	if !ok {
+		return false
+	}
+	seconds := milliseconds / 1_000
+	attrs["http.request.duration"] = seconds
+	return true
+}
+
+// Converts Milliseconds value to Seconds and sets as "http.server.request.duration"
+func appServiceHTTPLogTimeTakenMilliseconds(_ string, value any, attrs map[string]any) bool {
+	milliseconds, ok := tryParseFloat64(value)
+	if !ok {
+		return false
+	}
+	seconds := milliseconds / 1_000
+	attrs["http.server.request.duration"] = seconds
+	return true
+}
+
+func tryParseFloat64(value any) (float64, bool) {
+	switch v := value.(type) {
+	case float32:
+		return float64(v), true
+	case float64:
+		return v, true
+	case int:
+		return float64(v), true
+	case int32:
+		return float64(v), true
+	case int64:
+		return float64(v), true
+	case string:
+		f, err := strconv.ParseFloat(v, 64)
+		return f, err == nil
+	default:
+		return 0, false
+	}
+}
+
+func tryGetComplexConversion(category string, propertyName string) (ComplexConversion, bool) {
+	key := fmt.Sprintf("%s:%s", category, propertyName)
+	conversion, ok := conversions[key]
+	return conversion, ok
+}
diff --git a/pkg/translator/azurelogs/complex_conversions_test.go b/pkg/translator/azurelogs/complex_conversions_test.go
new file mode 100644
index 000000000000..453ad064f272
--- /dev/null
+++ b/pkg/translator/azurelogs/complex_conversions_test.go
@@ -0,0 +1,76 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package azurelogs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azurelogs"
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestFrontDoorAccessLogSecurityProtocol(t *testing.T) {
+	f, ok := tryGetComplexConversion("FrontDoorAccessLog", "securityProtocol")
+	assert.True(t, ok)
+	attrs := map[string]any{}
+	ok = f("securityProtocol", "TLS 1.2", attrs)
+	assert.True(t, ok)
+	protocolName, ok := attrs["tls.protocol.name"]
+	assert.True(t, ok)
+	// Protocol name is normalized to lower case
+	assert.Equal(t, "tls", protocolName)
+	protocolVersion, ok := attrs["tls.protocol.version"]
+	assert.True(t, ok)
+	assert.Equal(t, "1.2", protocolVersion)
+}
+
+func TestAzureCDNAccessLogSecurityProtocol(t *testing.T) {
+	f, ok := tryGetComplexConversion("AzureCdnAccessLog", "SecurityProtocol")
+	assert.True(t, ok)
+	attrs := map[string]any{}
+	ok = f("SecurityProtocol", "TLS 1.2", attrs)
+	assert.True(t, ok)
+	protocolName, ok := attrs["tls.protocol.name"]
+	assert.True(t, ok)
+	// Protocol name is normalized to lower case
+	assert.Equal(t, "tls", protocolName)
+	protocolVersion, ok := attrs["tls.protocol.version"]
+	assert.True(t, ok)
+	assert.Equal(t, "1.2", protocolVersion)
+}
+
+func TestAppServiceHTTPLogsProtocol(t *testing.T) {
+	f, ok := tryGetComplexConversion("AppServiceHTTPLogs", "Protocol")
+	assert.True(t, ok)
+	attrs := map[string]any{}
+	ok = f("Protocol", "HTTP/1.1", attrs)
+	assert.True(t, ok)
+	protocolName, ok := attrs["network.protocol.name"]
+	assert.True(t, ok)
+	assert.Equal(t, "http", protocolName)
+	protocolVersion, ok := attrs["network.protocol.version"]
+	assert.True(t, ok)
+	assert.Equal(t, "1.1", protocolVersion)
+}
+
+func TestFrontDoorHealthProbeLogDNSLatencyMicroseconds(t *testing.T) {
+	f, ok := tryGetComplexConversion("FrontDoorHealthProbeLog", "DNSLatencyMicroseconds")
+	assert.True(t, ok)
+	attrs := map[string]any{}
+	ok = f("DNSLatencyMicroseconds", 123456, attrs)
+	assert.True(t, ok)
+	duration, ok := attrs["dns.lookup.duration"].(float64)
+	assert.True(t, ok)
+	assert.Equal(t, 0.123456, duration)
+}
+
+func TestFrontDoorHealthProbeLogTotalLatencyMilliseconds(t *testing.T) {
+	f, ok := tryGetComplexConversion("FrontDoorHealthProbeLog", "totalLatencyMilliseconds")
+	assert.True(t, ok)
+	attrs := map[string]any{}
+	ok = f("totalLatencyMilliseconds", 123, attrs)
+	assert.True(t, ok)
+	duration, ok := attrs["http.request.duration"].(float64)
+	assert.True(t, ok)
+	assert.Equal(t, 0.123, duration)
+}
diff --git a/pkg/translator/azurelogs/go.mod b/pkg/translator/azurelogs/go.mod
new file mode 100644
index 000000000000..f401c2aedee8
--- /dev/null
+++ b/pkg/translator/azurelogs/go.mod
@@ -0,0 +1,44 @@
+module github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azurelogs
+
+go 1.22.0
+
+require (
+	github.com/json-iterator/go v1.1.12
+	github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.109.0
+	github.com/relvacode/iso8601 v1.4.0
+	github.com/stretchr/testify v1.9.0
+	go.opentelemetry.io/collector/component v0.109.1-0.20240918193345-a3c0565031b0
+	go.opentelemetry.io/collector/pdata v1.15.1-0.20240918193345-a3c0565031b0
+	go.opentelemetry.io/collector/semconv v0.109.1-0.20240918193345-a3c0565031b0
+	go.uber.org/goleak v1.3.0
+	go.uber.org/zap v1.27.0
+	golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f
+)
+
+require (
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.109.0 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	go.opentelemetry.io/collector/config/configtelemetry v0.109.1-0.20240918193345-a3c0565031b0 // indirect
+	go.opentelemetry.io/otel v1.30.0 // indirect
+	go.opentelemetry.io/otel/metric v1.30.0 // indirect
+	go.opentelemetry.io/otel/trace v1.30.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/net v0.28.0 // indirect
+	golang.org/x/sys v0.25.0 // indirect
+	golang.org/x/text v0.17.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
+	google.golang.org/grpc v1.66.2 // indirect
+	google.golang.org/protobuf v1.34.2 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+
+replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil => ../../pdatautil
+
+replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest => ../../pdatatest
+
+replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden => ../../golden
diff --git a/pkg/translator/azurelogs/go.sum b/pkg/translator/azurelogs/go.sum
new file mode 100644
index 000000000000..fbf55dbf8d70
--- /dev/null
+++ b/pkg/translator/azurelogs/go.sum
@@ -0,0 +1,103 @@
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/relvacode/iso8601 v1.4.0 h1:GsInVSEJfkYuirYFxa80nMLbH2aydgZpIf52gYZXUJs=
+github.com/relvacode/iso8601 v1.4.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/collector/component v0.109.1-0.20240918193345-a3c0565031b0 h1:EMCrB+XCK6L9Mm1Djf+K7G2hBp9i/30geloesMzhDm4=
+go.opentelemetry.io/collector/component v0.109.1-0.20240918193345-a3c0565031b0/go.mod h1:kMqUSLsNi4Bk/uYVAf9FNgXvqxVM1Jau3zaj6inIqlo=
+go.opentelemetry.io/collector/config/configtelemetry v0.109.1-0.20240918193345-a3c0565031b0 h1:ZpOgqkcRXuTVP03w9g9tlDMF5pgYApByZ390TbM3VyI=
+go.opentelemetry.io/collector/config/configtelemetry v0.109.1-0.20240918193345-a3c0565031b0/go.mod h1:R0MBUxjSMVMIhljuDHWIygzzJWQyZHXXWIgQNxcFwhc=
+go.opentelemetry.io/collector/pdata v1.15.1-0.20240918193345-a3c0565031b0 h1:DsH0WYkRYWjCg0my1gPqfCBRmsSI67Z2e+gTuOnPrvE=
+go.opentelemetry.io/collector/pdata v1.15.1-0.20240918193345-a3c0565031b0/go.mod h1:YZZJIt2ehxosYf/Y1pbvexjNWsIGNNrzzlCTO9jC1F4=
+go.opentelemetry.io/collector/semconv v0.109.1-0.20240918193345-a3c0565031b0 h1:NOf0YOdbHaCK+ej9NIUmBirHESltL44Ri5cYljg0jMc=
+go.opentelemetry.io/collector/semconv v0.109.1-0.20240918193345-a3c0565031b0/go.mod h1:zCJ5njhWpejR+A40kiEoeFm1xq1uzyZwMnRNX6/D82A=
+go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
+go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
+go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
+go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
+go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
+go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
+golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
+golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
+google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo=
+google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/translator/azurelogs/metadata.yaml b/pkg/translator/azurelogs/metadata.yaml
new file mode 100644
index 000000000000..3dfa1a43ee2c
--- /dev/null
+++ b/pkg/translator/azurelogs/metadata.yaml
@@ -0,0 +1,3 @@
+status:
+  codeowners:
+    active: [atoulme, cparkins, MikeGoldsmith]
diff --git a/pkg/translator/azurelogs/normalize.go b/pkg/translator/azurelogs/normalize.go
new file mode 100644
index 000000000000..3f9566ae7e88
--- /dev/null
+++ b/pkg/translator/azurelogs/normalize.go
@@ -0,0 +1,77 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package azurelogs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azurelogs"
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+func normalizeValue(key string, val any) any {
+	switch key {
+	case
+		"http.request.body.size",
+		"http.request.size",
+		"http.response.body.size",
+		"http.response.size",
+		"http.response.status_code",
+		"server.port":
+		return toInt(val)
+	case
+		"http.server.request.duration":
+		return toFloat(val)
+	case
+		"network.protocol.name":
+		return toLower(val)
+	}
+	return val
+}
+
+func toLower(value any) any {
+	switch v := value.(type) {
+	case string:
+		return strings.ToLower(v)
+	default:
+		return strings.ToLower(fmt.Sprint(value))
+	}
+}
+
+func toFloat(value any) any {
+	switch v := value.(type) {
+	case float64:
+		return v
+	case float32:
+		return float64(v)
+	case int:
+		return float64(v)
+	case int32:
+		return float64(v)
+	case int64:
+		return float64(v)
+	case string:
+		f, err := strconv.ParseFloat(v, 64)
+		if err == nil {
+			return f
+		}
+	}
+	return value
+}
+
+func toInt(value any) any {
+	switch v := value.(type) {
+	case int:
+		return int64(v)
+	case int32:
+		return int64(int(v))
+	case int64:
+		return value.(int64)
+	case string:
+		i, err := strconv.ParseInt(v, 10, 64)
+		if err == nil {
+			return i
+		}
+	}
+	return value
+}
diff --git a/pkg/translator/azurelogs/package_test.go b/pkg/translator/azurelogs/package_test.go
new file mode 100644
index 000000000000..d0e774bde369
--- /dev/null
+++ b/pkg/translator/azurelogs/package_test.go
@@ -0,0 +1,14 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package azurelogs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azurelogs"
+
+import (
+	"testing"
+
+	"go.uber.org/goleak"
+)
+
+func TestMain(m *testing.M) {
+	goleak.VerifyTestMain(m)
+}
diff --git a/pkg/translator/azurelogs/property_names.go b/pkg/translator/azurelogs/property_names.go
new file mode 100644
index 000000000000..bdb2380d3699
--- /dev/null
+++ b/pkg/translator/azurelogs/property_names.go
@@ -0,0 +1,298 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package azurelogs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azurelogs"
+
+var mappings = map[string]map[string]string{
+	"common": {},
+	"AzureCdnAccessLog": {
+		"BackendHostname":       "destination.address",       // If the request is being forwarded to a backend, this field represents the hostname of the backend. This field is blank if the request gets redirected or forwarded to a regional cache (when caching gets enabled for the routing rule).
+		"CacheStatus":           "",                          // For caching scenarios, this field defines the cache hit/miss at the POP
+		"ClientIp":              "client.address",            // The IP address of the client that made the request. If there was an X-Forwarded-For header in the request, then the Client IP is picked from the same.
+		"ClientPort":            "client.port",               // The IP port of the client that made the request.
+		"HttpMethod":            "http.request.method",       // HTTP method used by the request.
+		"HttpStatusCode":        "http.response.status_code", // The HTTP status code returned from the proxy. If a request to the origin timeouts the value for HttpStatusCode is set to 0.
+		"HttpStatusDetails":     "",                          // Resulting status on the request. Meaning of this string value can be found at a Status reference table.
+		"HttpVersion":           "network.protocol.version",  // Type of the request or connection.
+		"POP":                   "",                          // Short name of the edge where the request landed.
+		"RequestBytes":          "http.request.size",         // The size of the HTTP request message in bytes, including the request headers and the request body.
+		"RequestUri":            "url.full",                  // URI of the received request.
+		"ResponseBytes":         "http.response.size",        // Bytes sent by the backend server as the response.
+		"RoutingRuleName":       "",                          // The name of the routing rule that the request matched.
+		"RulesEngineMatchNames": "",                          // The names of the rules that the request matched.
+		"SecurityProtocol":      "",                          // handled by complex_conversions
+		"isReceivedFromClient":  "",                          // If true, it means that the request came from the client. If false, the request is a miss in the edge (child POP) and is responded from origin shield (parent POP).
+		"TimeTaken":             "",                          // The length of time from first byte of request into Azure Front Door to last byte of response out, in seconds.
+		"TrackingReference":     "az.service_request_id",     // The unique reference string that identifies a request served by Azure Front Door, also sent as X-Azure-Ref header to the client. Required for searching details in the access logs for a specific request.
+		"UserAgent":             "user_agent.original",       // The browser type that the client used.
+		"ErrorInfo":             "error.type",                // This field contains the specific type of error to narrow down troubleshooting area.
+		"TimeToFirstByte":       "",                          // The length of time in milliseconds from when Microsoft CDN receives the request to the time the first byte gets sent to the client. The time is measured only from the Microsoft side. Client-side data isn't measured.
+		"Result":                "",                          // SSLMismatchedSNI is a status code that signifies a successful request with a mismatch warning between the Server Name Indication (SNI) and the host header. This status code implies domain fronting, a technique that violates Azure Front Door's terms of service. Requests with SSLMismatchedSNI will be rejected after January 22, 2024.
+		"SNI":                   "",                          // This field specifies the Server Name Indication (SNI) that is sent during the TLS/SSL handshake. It can be used to identify the exact SNI value if there was a SSLMismatchedSNI status code. Additionally, it can be compared with the host value in the requestUri field to detect and resolve the mismatch issue.
+	},
+	"FrontDoorAccessLog": {
+		"trackingReference":   "az.service_request_id",
+		"httpMethod":          "http.request.method",
+		"httpVersion":         "network.protocol.version",
+		"requestUri":          "url.full",
+		"hostName":            "server.address",
+		"requestBytes":        "http.request.size",
+		"responseBytes":       "http.response.size",
+		"userAgent":           "user_agent.original",
+		"clientIp":            "client.address",
+		"clientPort":          "client.port",
+		"socketIp":            "network.peer.address",
+		"timeTaken":           "http.server.request.duration",
+		"requestProtocol":     "network.protocol.name",
+		"securityProtocol":    "", // handled by complex_conversions
+		"securityCipher":      "tls.cipher",
+		"securityCurves":      "tls.curve",
+		"endpoint":            "",
+		"httpStatusCode":      "http.response.status_code",
+		"pop":                 "",
+		"cacheStatus":         "",
+		"matchedRulesSetName": "",
+		"routeName":           "http.route",
+		"referer":             "http.request.header.referer",
+		"timeToFirstByte":     "",
+		"errorInfo":           "error.type",
+		"originURL":           "",
+		"originIP":            "",
+		"originName":          "",
+		"result":              "",
+		"sni":                 "",
+	},
+	"FrontDoorHealthProbeLog": {
+		"healthProbeId":                 "",
+		"POP":                           "",
+		"httpVerb":                      "http.request.method",
+		"result":                        "",
+		"httpStatusCode":                "http.response.status_code",
+		"probeURL":                      "url.full",
+		"originName":                    "",
+		"originIP":                      "server.address",
+		"totalLatencyMilliseconds":      "", // handled by complex_conversions
+		"connectionLatencyMilliseconds": "",
+		"DNSLatencyMicroseconds":        "", // handled by complex_conversions
+	},
+	"FrontdoorWebApplicationFirewallLog": {
+		"clientIP":          "client.address",
+		"clientPort":        "client.port",
+		"socketIP":          "network.peer.address",
+		"requestUri":        "url.full",
+		"ruleName":          "",
+		"policy":            "",
+		"action":            "",
+		"host":              "server.address",
+		"trackingReference": "az.service_request_id",
+		"policyMode":        "",
+	},
+	"AppServiceAppLogs": {
+		"_BilledSize":       "",                     // real	The record size in bytes
+		"Category":          "",                     // string	Log category name
+		"ContainerId":       "container.id",         // string	Application container id
+		"CustomLevel":       "",                     // string	Verbosity level of log
+		"ExceptionClass":    "exception.type",       // string	Application class from where log message is emitted
+		"Host":              "host.id",              // string	Host where the application is running
+		"_IsBillable":       "",                     // string	Specifies whether ingesting the data is billable. When _IsBillable is false ingestion isn't billed to your Azure account
+		"Level":             "",                     // string	Verbosity level of log mapped to standard levels (Informational, Warning, Error, or Critical)
+		"Logger":            "",                     // string	Application logger used to emit log message
+		"Message":           "",                     // string	Log message
+		"Method":            "code.function",        // string	Application Method from where log message is emitted
+		"OperationName":     "",                     // string	The name of the operation represented by this event.
+		"_ResourceId":       "",                     // string	A unique identifier for the resource that the record is associated with
+		"ResultDescription": "",                     // string	Log message description
+		"Source":            "code.filepath",        // string	Application source from where log message is emitted
+		"SourceSystem":      "",                     // string	The type of agent the event was collected by. For example, OpsManager for Windows agent, either direct connect or Operations Manager, Linux for all Linux agents, or Azure for Azure Diagnostics
+		"Stacktrace":        "exception.stacktrace", // string	Complete stack trace of the log message in case of exception
+		"StackTrace":        "exception.stacktrace", // string	Complete stack trace of the log message in case of exception
+		"_SubscriptionId":   "",                     // string	A unique identifier for the subscription that the record is associated with
+		"TenantId":          "",                     // string	The Log Analytics workspace ID
+		"TimeGenerated":     "",                     // datetime	Time when event is generated
+		"Type":              "",                     // string	The name of the table
+		"WebSiteInstanceId": "",                     // string	Instance ID of the application running
+	},
+	"AppServiceAuditLogs": {
+		"_BilledSize":     "",                      // real	The record size in bytes
+		"Category":        "",                      // string	Log category name
+		"_IsBillable":     "",                      // string	Specifies whether ingesting the data is billable. When _IsBillable is false ingestion isn't billed to your Azure account
+		"OperationName":   "",                      // string	Name of the operation
+		"Protocol":        "network.protocol.name", // string	Authentication protocol
+		"_ResourceId":     "",                      // string	A unique identifier for the resource that the record is associated with
+		"SourceSystem":    "",                      // string	The type of agent the event was collected by. For example, OpsManager for Windows agent, either direct connect or Operations Manager, Linux for all Linux agents, or Azure for Azure Diagnostics
+		"_SubscriptionId": "",                      // string	A unique identifier for the subscription that the record is associated with
+		"TenantId":        "",                      // string	The Log Analytics workspace ID
+		"TimeGenerated":   "",                      // datetime	Time when event is generated
+		"Type":            "",                      // string	The name of the table
+		"User":            "enduser.id",            // string	Username used for publishing access
+		"UserAddress":     "client.address",        // string	Client IP address of the publishing user
+		"UserDisplayName": "",                      // string	Email address of a user in case publishing was authorized via AAD authentication
+	},
+	"AppServiceAuthenticationLogs": {
+		"_BilledSize":          "",                          // real	The record size in bytes
+		"CorrelationId":        "",                          // string	The ID for correlated events.
+		"Details":              "",                          // string	The event details.
+		"HostName":             "",                          // string	The host name of the application.
+		"_IsBillable":          "",                          // string	Specifies whether ingesting the data is billable. When _IsBillable is false ingestion isn't billed to your Azure account
+		"Level":                "",                          // string	The level of log verbosity.
+		"Message":              "",                          // string	The log message.
+		"ModuleRuntimeVersion": "",                          // string	The version of App Service Authentication running.
+		"OperationName":        "",                          // string	The name of the operation represented by this event.
+		"_ResourceId":          "",                          // string	A unique identifier for the resource that the record is associated with
+		"SiteName":             "",                          // string	The runtime name of the application.
+		"SourceSystem":         "",                          // string	The type of agent the event was collected by. For example, OpsManager for Windows agent, either direct connect or Operations Manager, Linux for all Linux agents, or Azure for Azure Diagnostics
+		"StatusCode":           "http.response.status_code", // int	The HTTP status code of the operation.
+		"_SubscriptionId":      "",                          // string	A unique identifier for the subscription that the record is associated with
+		"SubStatusCode":        "",                          // int	The HTTP sub-status code of the request.
+		"TaskName":             "",                          // string	The name of the task being performed.
+		"TenantId":             "",                          // string	The Log Analytics workspace ID
+		"TimeGenerated":        "",                          // datetime	The timestamp (UTC) of when this event was generated.
+		"Type":                 "",                          // string	The name of the table
+	},
+	"AppServiceConsoleLogs": {
+		"_BilledSize":       "",             // real	The record size in bytes
+		"Category":          "",             // string	Log category name
+		"ContainerId":       "container.id", // string	Application container id
+		"Host":              "host.id",      // string	Host where the application is running
+		"_IsBillable":       "",             // string	Specifies whether ingesting the data is billable. When _IsBillable is false ingestion isn't billed to your Azure account
+		"Level":             "",             // string	Verbosity level of log
+		"OperationName":     "",             // string	The name of the operation represented by this event.
+		"_ResourceId":       "",             // string	A unique identifier for the resource that the record is associated with
+		"ResultDescription": "",             // string	Log message description
+		"SourceSystem":      "",             // string	The type of agent the event was collected by. For example, OpsManager for Windows agent, either direct connect or Operations Manager, Linux for all Linux agents, or Azure for Azure Diagnostics
+		"_SubscriptionId":   "",             // string	A unique identifier for the subscription that the record is associated with
+		"TenantId":          "",             // string	The Log Analytics workspace ID
+		"TimeGenerated":     "",             // datetime	Time when event is generated
+		"Type":              "",             // string	The name of the table
+	},
+	"AppServiceEnvironmentPlatformLogs": {
+		"_BilledSize":       "", // real	The record size in bytes
+		"Category":          "", // string
+		"_IsBillable":       "", // string	Specifies whether ingesting the data is billable. When _IsBillable is false ingestion isn't billed to your Azure account
+		"OperationName":     "", // string
+		"ResourceId":        "", // string
+		"_ResourceId":       "", // string	A unique identifier for the resource that the record is associated with
+		"ResultDescription": "", // string
+		"ResultType":        "", // string
+		"SourceSystem":      "", // string	The type of agent the event was collected by. For example, OpsManager for Windows agent, either direct connect or Operations Manager, Linux for all Linux agents, or Azure for Azure Diagnostics
+		"_SubscriptionId":   "", // string	A unique identifier for the subscription that the record is associated with
+		"TimeGenerated":     "", // datetime
+		"Type":              "", // string	The name of the table
+	},
+	"AppServiceFileAuditLogs": {
+		"_BilledSize":     "", // real	The record size in bytes
+		"Category":        "", // string	Log category name
+		"_IsBillable":     "", // string	Specifies whether ingesting the data is billable. When _IsBillable is false ingestion isn't billed to your Azure account
+		"OperationName":   "", // string	Operation performed on a file
+		"Path":            "", // string	Path to the file that was changed
+		"Process":         "", // string	Type of the process that change the file
+		"_ResourceId":     "", // string	A unique identifier for the resource that the record is associated with
+		"SourceSystem":    "", // string	The type of agent the event was collected by. For example, OpsManager for Windows agent, either direct connect or Operations Manager, Linux for all Linux agents, or Azure for Azure Diagnostics
+		"_SubscriptionId": "", // string	A unique identifier for the subscription that the record is associated with
+		"TenantId":        "", // string	The Log Analytics workspace ID
+		"TimeGenerated":   "", // datetime	Time when event is generated
+		"Type":            "", // string	The name of the table
+	},
+	"AppServiceHTTPLogs": {
+		"_BilledSize":     "",                             // real	The record size in bytes
+		"CIp":             "client.address",               // string	IP address of the client
+		"ComputerName":    "host.name",                    // string	The name of the server on which the log file entry was generated.
+		"Cookie":          "",                             // string	Cookie on HTTP request
+		"CsBytes":         "http.request.body.size",       // int	Number of bytes received by server
+		"CsHost":          "url.domain",                   // string	Host name header on HTTP request
+		"CsMethod":        "http.request.method",          // string	The request HTTP verb
+		"CsUriQuery":      "url.query",                    // string	URI query on HTTP request
+		"CsUriStem":       "url.path",                     // string	The target of the request
+		"CsUsername":      "",                             // string	The name of the authenticated user on HTTP request
+		"_IsBillable":     "",                             // string	Specifies whether ingesting the data is billable. When _IsBillable is false ingestion isn't billed to your Azure account
+		"Protocol":        "",                             // handled by complex_conversions
+		"Referer":         "http.request.header.referer",  // string	The site that the user last visited. This site provided a link to the current site
+		"_ResourceId":     "",                             // string	A unique identifier for the resource that the record is associated with
+		"Result":          "",                             // string	Success / Failure of HTTP request
+		"ScBytes":         "http.response.body.size",      // int	Number of bytes sent by server
+		"ScStatus":        "http.response.status_code",    // int	HTTP status code
+		"ScSubStatus":     "",                             // string	Sub-status error code on HTTP request
+		"ScWin32Status":   "",                             // string	Windows status code on HTTP request
+		"SourceSystem":    "",                             // string	The type of agent the event was collected by. For example, OpsManager for Windows agent, either direct connect or Operations Manager, Linux for all Linux agents, or Azure for Azure Diagnostics
+		"SPort":           "server.port",                  // string	Server port number
+		"_SubscriptionId": "",                             // string	A unique identifier for the subscription that the record is associated with
+		"TenantId":        "",                             // string	The Log Analytics workspace ID
+		"TimeGenerated":   "",                             // datetime	Time when event is generated
+		"TimeTaken":       "http.server.request.duration", // int	Time taken by HTTP request in milliseconds
+		"Type":            "",                             // string	The name of the table
+		"UserAgent":       "user_agent.original",          // string	User agent on HTTP request
+	},
+	"AppServiceIPSecAuditLogs": {
+		"_BilledSize":     "",                                     // real	The record size in bytes
+		"CIp":             "client.address",                       // string	IP address of the client
+		"CsHost":          "url.domain",                           // string	Host header of the HTTP request
+		"Details":         "",                                     // string	Additional information
+		"_IsBillable":     "",                                     // string	Specifies whether ingesting the data is billable. When _IsBillable is false ingestion isn't billed to your Azure account
+		"_ResourceId":     "",                                     // string	A unique identifier for the resource that the record is associated with
+		"Result":          "",                                     // string	The result whether the access is Allowed or Denied
+		"ServiceEndpoint": "",                                     // string	This indicates whether the access is via Virtual Network Service Endpoint communication
+		"SourceSystem":    "",                                     // string	The type of agent the event was collected by. For example, OpsManager for Windows agent, either direct connect or Operations Manager, Linux for all Linux agents, or Azure for Azure Diagnostics
+		"_SubscriptionId": "",                                     // string	A unique identifier for the subscription that the record is associated with
+		"TenantId":        "",                                     // string	The Log Analytics workspace ID
+		"TimeGenerated":   "",                                     // datetime	Time of the Http Request
+		"Type":            "",                                     // string	The name of the table
+		"XAzureFDID":      "http.request.header.x-azure-fdid",     // string	X-Azure-FDID header (Azure Frontdoor ID) of the HTTP request
+		"XFDHealthProbe":  "http.request.header.x-fd-healthprobe", // string	X-FD-HealthProbe (Azure Frontdoor Health Probe) of the HTTP request
+		"XForwardedFor":   "http.request.header.x-forwarded-for",  // string	X-Forwarded-For header of the HTTP request
+		"XForwardedHost":  "http.request.header.x-forwarded-host", // string	X-Forwarded-Host header of the HTTP request
+	},
+	"AppServicePlatformLogs": {
+		"ActivityId":      "",               // string	Activity ID to correlate events
+		"_BilledSize":     "",               // real	The record size in bytes
+		"containerId":     "container.id",   // string	Application container id
+		"containerName":   "container.name", // string	Application container id
+		"DeploymentId":    "",               // string	Deployment ID of the application deployment
+		"exception":       "error.type",     // string	Details of the exception
+		"Host":            "",               // string	Host where the application is running
+		"_IsBillable":     "",               // string	Specifies whether ingesting the data is billable. When _IsBillable is false ingestion isn't billed to your Azure account
+		"Level":           "",               // string	Level of log verbosity
+		"Message":         "",               // string	Log message
+		"OperationName":   "",               // string	The name of the operation represented by this event.
+		"_ResourceId":     "",               // string	A unique identifier for the resource that the record is associated with
+		"SourceSystem":    "",               // string	The type of agent the event was collected by. For example, OpsManager for Windows agent, either direct connect or Operations Manager, Linux for all Linux agents, or Azure for Azure Diagnostics
+		"StackTrace":      "",               // string	Stack trace for the exception
+		"_SubscriptionId": "",               // string	A unique identifier for the subscription that the record is associated with
+		"TenantId":        "",               // string	The Log Analytics workspace ID
+		"TimeGenerated":   "",               // datetime	Time when event is generated
+		"Type":            "",               // string	The name of the table
+	},
+	"AppServiceServerlessSecurityPluginData": {
+		"_BilledSize":     "", // real	The record size in bytes
+		"Index":           "", // int	Available when multiple payloads exist for the same message. In that case, payloads share the same SlSecRequestId and Index defines the chronological order of payloads.
+		"_IsBillable":     "", // string	Specifies whether ingesting the data is billable. When _IsBillable is false ingestion isn't billed to your Azure account
+		"MsgVersion":      "", // string	The version of the message schema. Used to make code changes backward- and forward- compatible.
+		"Payload":         "", // dynamic	An array of messages, where each one is a JSON string.
+		"PayloadType":     "", // string	The type of the payload. Mostly used to distinguish between messages meant for different types of security analysis.
+		"_ResourceId":     "", // string	A unique identifier for the resource that the record is associated with
+		"Sender":          "", // string	The name of the component that published this message. Almost always will be the name of the plugin, but can also be platform.
+		"SlSecMetadata":   "", // dynamic	Contains details about the resource like the deployment ID, runtime info, website info, OS, etc.
+		"SlSecProps":      "", // dynamic	Contains other details that might be needed for debugging end-to-end requests, e.g., slsec nuget version.
+		"SlSecRequestId":  "", // string	The ingestion request ID used for identifying the message and the request for diagnostics and debugging.
+		"SourceSystem":    "", // string	The type of agent the event was collected by. For example, OpsManager for Windows agent, either direct connect or Operations Manager, Linux for all Linux agents, or Azure for Azure Diagnostics
+		"_SubscriptionId": "", // string	A unique identifier for the subscription that the record is associated with
+		"TenantId":        "", // string	The Log Analytics workspace ID
+		"TimeGenerated":   "", // datetime	The date and time (UTC) this message was created on the node.
+		"Type":            "", // string	The name of the table
+	},
+}
+
+func resourceLogKeyToSemConvKey(azName string, category string) (string, bool) {
+	mapping, ok := mappings[category]
+	if ok {
+		if mapped := mapping[azName]; mapped != "" {
+			return mapped, true
+		}
+	}
+
+	mapping = mappings["common"]
+	if name := mapping[azName]; name != "" {
+		return name, true
+	}
+
+	return "", false
+}
diff --git a/pkg/translator/azurelogs/resourcelogs_to_logs.go b/pkg/translator/azurelogs/resourcelogs_to_logs.go
new file mode 100644
index 000000000000..5d18bc1d3b63
--- /dev/null
+++ b/pkg/translator/azurelogs/resourcelogs_to_logs.go
@@ -0,0 +1,256 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package azurelogs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azurelogs"
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"strconv"
+
+	jsoniter "github.com/json-iterator/go"
+	"github.com/relvacode/iso8601"
+	"go.opentelemetry.io/collector/pdata/pcommon"
+	"go.opentelemetry.io/collector/pdata/plog"
+	conventions "go.opentelemetry.io/collector/semconv/v1.22.0"
+	"go.uber.org/zap"
+	"golang.org/x/exp/slices"
+)
+
+const (
+	// Constants for OpenTelemetry Specs
+	scopeName = "otelcol/azureresourcelogs"
+
+	// Constants for Azure Log Record Attributes
+	// TODO: Remove once these are available in semconv
+	eventName          = "event.name"
+	eventNameValue     = "az.resource.log"
+	networkPeerAddress = "network.peer.address"
+
+	// Constants for Azure Log Record body fields
+	azureCategory          = "category"
+	azureCorrelationID     = "correlation.id"
+	azureDuration          = "duration"
+	azureIdentity          = "identity"
+	azureOperationName     = "operation.name"
+	azureOperationVersion  = "operation.version"
+	azureProperties        = "properties"
+	azureResultType        = "result.type"
+	azureResultSignature   = "result.signature"
+	azureResultDescription = "result.description"
+	azureTenantID          = "tenant.id"
+)
+
+var (
+	errMissingTimestamp = errors.New("missing timestamp")
+)
+
+// azureRecords represents an array of Azure log records
+// as exported via an Azure Event Hub
+type azureRecords struct {
+	Records []azureLogRecord `json:"records"`
+}
+
+// azureLogRecord represents a single Azure log following
+// the common schema:
+// https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/resource-logs-schema
+type azureLogRecord struct {
+	Time              string       `json:"time"`
+	Timestamp         string       `json:"timeStamp"`
+	ResourceID        string       `json:"resourceId"`
+	TenantID          *string      `json:"tenantId"`
+	OperationName     string       `json:"operationName"`
+	OperationVersion  *string      `json:"operationVersion"`
+	Category          string       `json:"category"`
+	ResultType        *string      `json:"resultType"`
+	ResultSignature   *string      `json:"resultSignature"`
+	ResultDescription *string      `json:"resultDescription"`
+	DurationMs        *json.Number `json:"durationMs"`
+	CallerIPAddress   *string      `json:"callerIpAddress"`
+	CorrelationID     *string      `json:"correlationId"`
+	Identity          *any         `json:"identity"`
+	Level             *json.Number `json:"Level"`
+	Location          *string      `json:"location"`
+	Properties        *any         `json:"properties"`
+}
+
+var _ plog.Unmarshaler = (*ResourceLogsUnmarshaler)(nil)
+
+type ResourceLogsUnmarshaler struct {
+	Version string
+	Logger  *zap.Logger
+}
+
+func (r ResourceLogsUnmarshaler) UnmarshalLogs(buf []byte) (plog.Logs, error) {
+	l := plog.NewLogs()
+
+	var azureLogs azureRecords
+	decoder := jsoniter.NewDecoder(bytes.NewReader(buf))
+	if err := decoder.Decode(&azureLogs); err != nil {
+		return l, err
+	}
+
+	var resourceIDs []string
+	azureResourceLogs := make(map[string][]azureLogRecord)
+	for _, azureLog := range azureLogs.Records {
+		azureResourceLogs[azureLog.ResourceID] = append(azureResourceLogs[azureLog.ResourceID], azureLog)
+		keyExists := slices.Contains(resourceIDs, azureLog.ResourceID)
+		if !keyExists {
+			resourceIDs = append(resourceIDs, azureLog.ResourceID)
+		}
+	}
+
+	for _, resourceID := range resourceIDs {
+		logs := azureResourceLogs[resourceID]
+		resourceLogs := l.ResourceLogs().AppendEmpty()
+		scopeLogs := resourceLogs.ScopeLogs().AppendEmpty()
+		scopeLogs.Scope().SetName(scopeName)
+		scopeLogs.Scope().SetVersion(r.Version)
+		logRecords := scopeLogs.LogRecords()
+
+		for i := 0; i < len(logs); i++ {
+			log := logs[i]
+			nanos, err := getTimestamp(log)
+			if err != nil {
+				r.Logger.Warn("Unable to convert timestamp from log", zap.String("timestamp", log.Time))
+				continue
+			}
+
+			lr := logRecords.AppendEmpty()
+			lr.SetTimestamp(nanos)
+
+			if log.Level != nil {
+				severity := asSeverity(*log.Level)
+				lr.SetSeverityNumber(severity)
+				lr.SetSeverityText(log.Level.String())
+			}
+
+			lr.Attributes().PutStr(conventions.AttributeCloudResourceID, resourceID)
+			lr.Attributes().PutStr(conventions.AttributeCloudProvider, conventions.AttributeCloudProviderAzure)
+			lr.Attributes().PutStr(eventName, eventNameValue)
+
+			if err := lr.Body().FromRaw(extractRawAttributes(log)); err != nil {
+				return l, err
+			}
+		}
+	}
+
+	return l, nil
+}
+
+func getTimestamp(record azureLogRecord) (pcommon.Timestamp, error) {
+	if record.Time != "" {
+		return asTimestamp(record.Time)
+	} else if record.Timestamp != "" {
+		return asTimestamp(record.Timestamp)
+	}
+
+	return 0, errMissingTimestamp
+}
+
+// asTimestamp will parse an ISO8601 string into an OpenTelemetry
+// nanosecond timestamp. If the string cannot be parsed, it will
+// return zero and the error.
+func asTimestamp(s string) (pcommon.Timestamp, error) {
+	t, err := iso8601.ParseString(s)
+	if err != nil {
+		return 0, err
+	}
+
+	return pcommon.Timestamp(t.UnixNano()), nil
+}
+
+// asSeverity converts the Azure log level to equivalent
+// OpenTelemetry severity numbers. If the log level is not
+// valid, then the 'Unspecified' value is returned.
+func asSeverity(number json.Number) plog.SeverityNumber {
+	switch number.String() {
+	case "Informational":
+		return plog.SeverityNumberInfo
+	case "Warning":
+		return plog.SeverityNumberWarn
+	case "Error":
+		return plog.SeverityNumberError
+	case "Critical":
+		return plog.SeverityNumberFatal
+	default:
+		var levelNumber, _ = number.Int64()
+		if levelNumber > 0 {
+			return plog.SeverityNumber(levelNumber)
+		}
+
+		return plog.SeverityNumberUnspecified
+	}
+}
+
+func extractRawAttributes(log azureLogRecord) map[string]any {
+	var attrs = map[string]any{}
+
+	attrs[azureCategory] = log.Category
+	setIf(attrs, azureCorrelationID, log.CorrelationID)
+	if log.DurationMs != nil {
+		duration, err := strconv.ParseInt(log.DurationMs.String(), 10, 64)
+		if err == nil {
+			attrs[azureDuration] = duration
+		}
+	}
+	if log.Identity != nil {
+		attrs[azureIdentity] = *log.Identity
+	}
+	attrs[azureOperationName] = log.OperationName
+	setIf(attrs, azureOperationVersion, log.OperationVersion)
+
+	if log.Properties != nil {
+		copyPropertiesAndApplySemanticConventions(log.Category, log.Properties, attrs)
+	}
+
+	setIf(attrs, azureResultDescription, log.ResultDescription)
+	setIf(attrs, azureResultSignature, log.ResultSignature)
+	setIf(attrs, azureResultType, log.ResultType)
+	setIf(attrs, azureTenantID, log.TenantID)
+
+	setIf(attrs, conventions.AttributeCloudRegion, log.Location)
+	setIf(attrs, networkPeerAddress, log.CallerIPAddress)
+	return attrs
+}
+
+func copyPropertiesAndApplySemanticConventions(category string, properties *any, attrs map[string]any) {
+	if properties == nil {
+		return
+	}
+
+	// TODO: check if this is a valid JSON string and parse it?
+	switch p := (*properties).(type) {
+	case map[string]any:
+		attrsProps := map[string]any{}
+
+		for k, v := range p {
+			// Check for a complex conversion, e.g. AppServiceHTTPLogs.Protocol
+			if complexConversion, ok := tryGetComplexConversion(category, k); ok {
+				if complexConversion(k, v, attrs) {
+					continue
+				}
+			}
+			// Check for an equivalent Semantic Convention key
+			if otelKey, ok := resourceLogKeyToSemConvKey(k, category); ok {
+				attrs[otelKey] = normalizeValue(otelKey, v)
+			} else {
+				attrsProps[k] = v
+			}
+		}
+
+		if len(attrsProps) > 0 {
+			attrs[azureProperties] = attrsProps
+		}
+	default:
+		// otherwise, just add the properties as-is
+		attrs[azureProperties] = *properties
+	}
+}
+
+func setIf(attrs map[string]any, key string, value *string) {
+	if value != nil && *value != "" {
+		attrs[key] = *value
+	}
+}
diff --git a/pkg/translator/azurelogs/resourcelogs_to_logs_test.go b/pkg/translator/azurelogs/resourcelogs_to_logs_test.go
new file mode 100644
index 000000000000..70ca49070129
--- /dev/null
+++ b/pkg/translator/azurelogs/resourcelogs_to_logs_test.go
@@ -0,0 +1,742 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package azurelogs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azurelogs"
+
+import (
+	"encoding/json"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"go.opentelemetry.io/collector/component"
+	"go.opentelemetry.io/collector/pdata/pcommon"
+	"go.opentelemetry.io/collector/pdata/plog"
+	conventions "go.opentelemetry.io/collector/semconv/v1.22.0"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/plogtest"
+)
+
+var testBuildInfo = component.BuildInfo{
+	Version: "1.2.3",
+}
+
+var minimumLogRecord = func() plog.LogRecord {
+	lr := plog.NewLogs().ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords().AppendEmpty()
+
+	ts, _ := asTimestamp("2022-11-11T04:48:27.6767145Z")
+	lr.SetTimestamp(ts)
+	lr.Attributes().PutStr(conventions.AttributeCloudProvider, conventions.AttributeCloudProviderAzure)
+	lr.Attributes().PutStr(conventions.AttributeCloudResourceID, "/RESOURCE_ID")
+	lr.Attributes().PutStr(eventName, eventNameValue)
+
+	body := lr.Body().SetEmptyMap()
+	body.PutStr(azureOperationName, "SecretGet")
+	body.PutStr(azureCategory, "AuditEvent")
+	body.CopyTo(lr.Body().Map())
+
+	return lr
+}()
+
+var maximumLogRecord1 = func() plog.LogRecord {
+	lr := plog.NewLogs().ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords().AppendEmpty()
+
+	ts, _ := asTimestamp("2022-11-11T04:48:27.6767145Z")
+	lr.SetTimestamp(ts)
+	lr.SetSeverityNumber(plog.SeverityNumberWarn)
+	lr.SetSeverityText("Warning")
+	guid := "607964b6-41a5-4e24-a5db-db7aab3b9b34"
+
+	lr.Attributes().PutStr(conventions.AttributeCloudProvider, conventions.AttributeCloudProviderAzure)
+	lr.Attributes().PutStr(conventions.AttributeCloudResourceID, "/RESOURCE_ID-1")
+	lr.Attributes().PutStr(eventName, eventNameValue)
+
+	body := lr.Body().SetEmptyMap()
+	body.PutStr(azureTenantID, "/TENANT_ID")
+	body.PutStr(azureOperationName, "SecretGet")
+	body.PutStr(azureOperationVersion, "7.0")
+	body.PutStr(azureCategory, "AuditEvent")
+	body.PutStr(azureCorrelationID, guid)
+	body.PutStr(azureResultType, "Success")
+	body.PutStr(azureResultSignature, "Signature")
+	body.PutStr(azureResultDescription, "Description")
+	body.PutInt(azureDuration, 1234)
+	body.PutStr(networkPeerAddress, "127.0.0.1")
+	body.PutStr(conventions.AttributeCloudRegion, "ukso")
+	body.PutEmptyMap(azureIdentity).PutEmptyMap("claim").PutStr("oid", guid)
+
+	properties := body.PutEmptyMap(azureProperties)
+	properties.PutStr("string", "string")
+	properties.PutDouble("int", 429)
+	properties.PutDouble("float", 3.14)
+	properties.PutBool("bool", false)
+
+	return lr
+}()
+
+var maximumLogRecord2 = func() []plog.LogRecord {
+	sl := plog.NewLogs().ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty()
+	lr := sl.LogRecords().AppendEmpty()
+	lr2 := sl.LogRecords().AppendEmpty()
+
+	ts, _ := asTimestamp("2022-11-11T04:48:29.6767145Z")
+	lr.SetTimestamp(ts)
+	lr.SetSeverityNumber(plog.SeverityNumberWarn)
+	lr.SetSeverityText("Warning")
+	guid := "96317703-2132-4a8d-a5d7-e18d2f486783"
+
+	lr.Attributes().PutStr(conventions.AttributeCloudProvider, conventions.AttributeCloudProviderAzure)
+	lr.Attributes().PutStr(conventions.AttributeCloudResourceID, "/RESOURCE_ID-2")
+	lr.Attributes().PutStr(eventName, eventNameValue)
+
+	body := lr.Body().SetEmptyMap()
+	body.PutStr(azureTenantID, "/TENANT_ID")
+	body.PutStr(azureOperationName, "SecretSet")
+	body.PutStr(azureOperationVersion, "7.0")
+	body.PutStr(azureCategory, "AuditEvent")
+	body.PutStr(azureCorrelationID, guid)
+	body.PutStr(azureResultType, "Success")
+	body.PutStr(azureResultSignature, "Signature")
+	body.PutStr(azureResultDescription, "Description")
+	body.PutInt(azureDuration, 4321)
+	body.PutStr(networkPeerAddress, "127.0.0.1")
+	body.PutStr(conventions.AttributeCloudRegion, "ukso")
+
+	body.PutEmptyMap(azureIdentity).PutEmptyMap("claim").PutStr("oid", guid)
+	properties := body.PutEmptyMap(azureProperties)
+	properties.PutStr("string", "string")
+	properties.PutDouble("int", 924)
+	properties.PutDouble("float", 41.3)
+	properties.PutBool("bool", true)
+
+	ts, _ = asTimestamp("2022-11-11T04:48:31.6767145Z")
+	lr2.SetTimestamp(ts)
+	lr2.SetSeverityNumber(plog.SeverityNumberWarn)
+	lr2.SetSeverityText("Warning")
+	guid = "4ae807da-39d9-4327-b5b4-0ab685a57f9a"
+
+	lr2.Attributes().PutStr(conventions.AttributeCloudProvider, conventions.AttributeCloudProviderAzure)
+	lr2.Attributes().PutStr(conventions.AttributeCloudResourceID, "/RESOURCE_ID-2")
+	lr2.Attributes().PutStr(eventName, eventNameValue)
+
+	body2 := lr2.Body().SetEmptyMap()
+	body2.PutStr(azureTenantID, "/TENANT_ID")
+	body2.PutStr(azureOperationName, "SecretGet")
+	body2.PutStr(azureOperationVersion, "7.0")
+	body2.PutStr(azureCategory, "AuditEvent")
+	body2.PutStr(azureCorrelationID, guid)
+	body2.PutStr(azureResultType, "Success")
+	body2.PutStr(azureResultSignature, "Signature")
+	body2.PutStr(azureResultDescription, "Description")
+	body2.PutInt(azureDuration, 321)
+	body2.PutStr(networkPeerAddress, "127.0.0.1")
+	body2.PutStr(conventions.AttributeCloudRegion, "ukso")
+
+	body2.PutEmptyMap(azureIdentity).PutEmptyMap("claim").PutStr("oid", guid)
+	properties = body2.PutEmptyMap(azureProperties)
+	properties.PutStr("string", "string")
+	properties.PutDouble("int", 925)
+	properties.PutDouble("float", 41.4)
+	properties.PutBool("bool", false)
+
+	var records []plog.LogRecord
+	return append(records, lr, lr2)
+}()
+
+var badLevelLogRecord = func() plog.LogRecord {
+	lr := plog.NewLogs().ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords().AppendEmpty()
+
+	ts, _ := asTimestamp("2023-10-26T14:22:43.3416357Z")
+	lr.SetTimestamp(ts)
+	lr.SetSeverityNumber(plog.SeverityNumberTrace4)
+	lr.SetSeverityText("4")
+	guid := "128bc026-5ead-40c7-8853-ebb32bc077a3"
+
+	lr.Attributes().PutStr(conventions.AttributeCloudProvider, conventions.AttributeCloudProviderAzure)
+	lr.Attributes().PutStr(conventions.AttributeCloudResourceID, "/RESOURCE_ID")
+	lr.Attributes().PutStr(eventName, eventNameValue)
+
+	body := lr.Body().SetEmptyMap()
+	body.PutStr(azureOperationName, "Microsoft.ApiManagement/GatewayLogs")
+	body.PutStr(azureCategory, "GatewayLogs")
+	body.PutStr(azureCorrelationID, guid)
+	body.PutStr(azureResultType, "Succeeded")
+	body.PutInt(azureDuration, 243)
+	body.PutStr(networkPeerAddress, "13.14.15.16")
+	body.PutStr(conventions.AttributeCloudRegion, "West US")
+
+	properties := body.PutEmptyMap(azureProperties)
+	properties.PutStr("method", "GET")
+	properties.PutStr("url", "https://api.azure-api.net/sessions")
+	properties.PutDouble("backendResponseCode", 200)
+	properties.PutDouble("responseCode", 200)
+	properties.PutDouble("responseSize", 102945)
+	properties.PutStr("cache", "none")
+	properties.PutDouble("backendTime", 54)
+	properties.PutDouble("requestSize", 632)
+	properties.PutStr("apiId", "demo-api")
+	properties.PutStr("operationId", "GetSessions")
+	properties.PutStr("apimSubscriptionId", "master")
+	properties.PutDouble("clientTime", 190)
+	properties.PutStr("clientProtocol", "HTTP/1.1")
+	properties.PutStr("backendProtocol", "HTTP/1.1")
+	properties.PutStr("apiRevision", "1")
+	properties.PutStr("clientTlsVersion", "1.2")
+	properties.PutStr("backendMethod", "GET")
+	properties.PutStr("backendUrl", "https://api.azurewebsites.net/sessions")
+	return lr
+}()
+
+var badTimeLogRecord = func() plog.LogRecord {
+	lr := plog.NewLogs().ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords().AppendEmpty()
+
+	ts, _ := asTimestamp("2021-10-14T22:17:11+00:00")
+	lr.SetTimestamp(ts)
+
+	lr.Attributes().PutStr(conventions.AttributeCloudProvider, conventions.AttributeCloudProviderAzure)
+	lr.Attributes().PutStr(conventions.AttributeCloudResourceID, "/RESOURCE_ID")
+	lr.Attributes().PutStr(eventName, eventNameValue)
+
+	body := lr.Body().SetEmptyMap()
+	body.PutStr(azureOperationName, "ApplicationGatewayAccess")
+	body.PutStr(azureCategory, "ApplicationGatewayAccessLog")
+
+	properties := body.PutEmptyMap(azureProperties)
+	properties.PutStr("instanceId", "appgw_2")
+	properties.PutStr("clientIP", "185.42.129.24")
+	properties.PutDouble("clientPort", 45057)
+	properties.PutStr("httpMethod", "GET")
+	properties.PutStr("originalRequestUriWithArgs", "/")
+	properties.PutStr("requestUri", "/")
+	properties.PutStr("requestQuery", "")
+	properties.PutStr("userAgent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36")
+	properties.PutDouble("httpStatus", 200)
+	properties.PutStr("httpVersion", "HTTP/1.1")
+	properties.PutDouble("receivedBytes", 184)
+	properties.PutDouble("sentBytes", 466)
+	properties.PutDouble("clientResponseTime", 0)
+	properties.PutDouble("timeTaken", 0.034)
+	properties.PutStr("WAFEvaluationTime", "0.000")
+	properties.PutStr("WAFMode", "Detection")
+	properties.PutStr("transactionId", "592d1649f75a8d480a3c4dc6a975309d")
+	properties.PutStr("sslEnabled", "on")
+	properties.PutStr("sslCipher", "ECDHE-RSA-AES256-GCM-SHA384")
+	properties.PutStr("sslProtocol", "TLSv1.2")
+	properties.PutStr("sslClientVerify", "NONE")
+	properties.PutStr("sslClientCertificateFingerprint", "")
+	properties.PutStr("sslClientCertificateIssuerName", "")
+	properties.PutStr("serverRouted", "52.239.221.65:443")
+	properties.PutStr("serverStatus", "200")
+	properties.PutStr("serverResponseLatency", "0.028")
+	properties.PutStr("upstreamSourcePort", "21564")
+	properties.PutStr("originalHost", "20.110.30.194")
+	properties.PutStr("host", "20.110.30.194")
+	return lr
+}()
+
+func TestAsTimestamp(t *testing.T) {
+	timestamp := "2022-11-11T04:48:27.6767145Z"
+	nanos, err := asTimestamp(timestamp)
+	assert.NoError(t, err)
+	assert.Less(t, pcommon.Timestamp(0), nanos)
+
+	timestamp = "invalid-time"
+	nanos, err = asTimestamp(timestamp)
+	assert.Error(t, err)
+	assert.Equal(t, pcommon.Timestamp(0), nanos)
+}
+
+func TestAsSeverity(t *testing.T) {
+	tests := map[string]plog.SeverityNumber{
+		"Informational": plog.SeverityNumberInfo,
+		"Warning":       plog.SeverityNumberWarn,
+		"Error":         plog.SeverityNumberError,
+		"Critical":      plog.SeverityNumberFatal,
+		"unknown":       plog.SeverityNumberUnspecified,
+	}
+
+	for input, expected := range tests {
+		t.Run(input, func(t *testing.T) {
+			assert.Equal(t, expected, asSeverity(json.Number(input)))
+		})
+	}
+}
+
+func TestSetIf(t *testing.T) {
+	m := map[string]any{}
+
+	setIf(m, "key", nil)
+	actual, found := m["key"]
+	assert.False(t, found)
+	assert.Nil(t, actual)
+
+	v := ""
+	setIf(m, "key", &v)
+	actual, found = m["key"]
+	assert.False(t, found)
+	assert.Nil(t, actual)
+
+	v = "ok"
+	setIf(m, "key", &v)
+	actual, found = m["key"]
+	assert.True(t, found)
+	assert.Equal(t, "ok", actual)
+}
+
+func TestExtractRawAttributes(t *testing.T) {
+	badDuration := json.Number("invalid")
+	goodDuration := json.Number("1234")
+
+	tenantID := "tenant.id"
+	operationVersion := "operation.version"
+	resultType := "result.type"
+	resultSignature := "result.signature"
+	resultDescription := "result.description"
+	callerIPAddress := "127.0.0.1"
+	correlationID := "edb70d1a-eec2-4b4c-b2f4-60e3510160ee"
+	level := json.Number("Informational")
+	location := "location"
+
+	identity := any("someone")
+
+	properties := any(map[string]any{
+		"a": uint64(1),
+		"b": true,
+		"c": 1.23,
+		"d": "ok",
+	})
+
+	stringProperties := any("str")
+	intProperties := any(1)
+	jsonProperties := any("{\"a\": 1, \"b\": true, \"c\": 1.23, \"d\": \"ok\"}")
+
+	tests := []struct {
+		name     string
+		log      azureLogRecord
+		expected map[string]any
+	}{
+		{
+			name: "minimal",
+			log: azureLogRecord{
+				Time:          "",
+				ResourceID:    "resource.id",
+				OperationName: "operation.name",
+				Category:      "category",
+				DurationMs:    &badDuration,
+			},
+			expected: map[string]any{
+				azureOperationName: "operation.name",
+				azureCategory:      "category",
+			},
+		},
+		{
+			name: "bad-duration",
+			log: azureLogRecord{
+				Time:          "",
+				ResourceID:    "resource.id",
+				OperationName: "operation.name",
+				Category:      "category",
+				DurationMs:    &badDuration,
+			},
+			expected: map[string]any{
+				azureOperationName: "operation.name",
+				azureCategory:      "category",
+			},
+		},
+		{
+			name: "everything",
+			log: azureLogRecord{
+				Time:              "",
+				ResourceID:        "resource.id",
+				TenantID:          &tenantID,
+				OperationName:     "operation.name",
+				OperationVersion:  &operationVersion,
+				Category:          "category",
+				ResultType:        &resultType,
+				ResultSignature:   &resultSignature,
+				ResultDescription: &resultDescription,
+				DurationMs:        &goodDuration,
+				CallerIPAddress:   &callerIPAddress,
+				CorrelationID:     &correlationID,
+				Identity:          &identity,
+				Level:             &level,
+				Location:          &location,
+				Properties:        &properties,
+			},
+			expected: map[string]any{
+				azureTenantID:                    "tenant.id",
+				azureOperationName:               "operation.name",
+				azureOperationVersion:            "operation.version",
+				azureCategory:                    "category",
+				azureCorrelationID:               correlationID,
+				azureResultType:                  "result.type",
+				azureResultSignature:             "result.signature",
+				azureResultDescription:           "result.description",
+				azureDuration:                    int64(1234),
+				networkPeerAddress:               "127.0.0.1",
+				azureIdentity:                    "someone",
+				conventions.AttributeCloudRegion: "location",
+				azureProperties:                  properties,
+			},
+		},
+		{
+			name: "nil properties",
+			log: azureLogRecord{
+				Time:          "",
+				ResourceID:    "resource.id",
+				OperationName: "operation.name",
+				Category:      "category",
+				DurationMs:    &badDuration,
+				Properties:    nil,
+			},
+			expected: map[string]any{
+				azureOperationName: "operation.name",
+				azureCategory:      "category",
+			},
+		},
+		{
+			name: "string properties",
+			log: azureLogRecord{
+				Time:          "",
+				ResourceID:    "resource.id",
+				OperationName: "operation.name",
+				Category:      "category",
+				DurationMs:    &badDuration,
+				Properties:    &stringProperties,
+			},
+			expected: map[string]any{
+				azureOperationName: "operation.name",
+				azureCategory:      "category",
+				azureProperties:    "str",
+			},
+		},
+		{
+			name: "int properties",
+			log: azureLogRecord{
+				Time:          "",
+				ResourceID:    "resource.id",
+				OperationName: "operation.name",
+				Category:      "category",
+				DurationMs:    &badDuration,
+				Properties:    &intProperties,
+			},
+			expected: map[string]any{
+				azureOperationName: "operation.name",
+				azureCategory:      "category",
+				azureProperties:    1,
+			},
+		},
+		{
+			name: "json properties",
+			log: azureLogRecord{
+				Time:          "",
+				ResourceID:    "resource.id",
+				OperationName: "operation.name",
+				Category:      "category",
+				DurationMs:    &badDuration,
+				Properties:    &jsonProperties,
+			},
+			expected: map[string]any{
+				azureOperationName: "operation.name",
+				azureCategory:      "category",
+				azureProperties:    "{\"a\": 1, \"b\": true, \"c\": 1.23, \"d\": \"ok\"}",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.expected, extractRawAttributes(tt.log))
+		})
+	}
+
+}
+
+func TestUnmarshalLogs(t *testing.T) {
+	expectedMinimum := plog.NewLogs()
+	resourceLogs := expectedMinimum.ResourceLogs().AppendEmpty()
+	scopeLogs := resourceLogs.ScopeLogs().AppendEmpty()
+	scopeLogs.Scope().SetName("otelcol/azureresourcelogs")
+	scopeLogs.Scope().SetVersion(testBuildInfo.Version)
+	lr := scopeLogs.LogRecords().AppendEmpty()
+	minimumLogRecord.CopyTo(lr)
+
+	expectedMinimum2 := plog.NewLogs()
+	resourceLogs = expectedMinimum2.ResourceLogs().AppendEmpty()
+	scopeLogs = resourceLogs.ScopeLogs().AppendEmpty()
+	scopeLogs.Scope().SetName("otelcol/azureresourcelogs")
+	scopeLogs.Scope().SetVersion(testBuildInfo.Version)
+	logRecords := scopeLogs.LogRecords()
+	lr = logRecords.AppendEmpty()
+	minimumLogRecord.CopyTo(lr)
+	lr = logRecords.AppendEmpty()
+	minimumLogRecord.CopyTo(lr)
+
+	expectedMaximum := plog.NewLogs()
+	resourceLogs = expectedMaximum.ResourceLogs().AppendEmpty()
+	scopeLogs = resourceLogs.ScopeLogs().AppendEmpty()
+	scopeLogs.Scope().SetName("otelcol/azureresourcelogs")
+	scopeLogs.Scope().SetVersion(testBuildInfo.Version)
+	lr = scopeLogs.LogRecords().AppendEmpty()
+	maximumLogRecord1.CopyTo(lr)
+
+	resourceLogs = expectedMaximum.ResourceLogs().AppendEmpty()
+	scopeLogs = resourceLogs.ScopeLogs().AppendEmpty()
+	scopeLogs.Scope().SetName("otelcol/azureresourcelogs")
+	scopeLogs.Scope().SetVersion(testBuildInfo.Version)
+	lr = scopeLogs.LogRecords().AppendEmpty()
+	lr2 := scopeLogs.LogRecords().AppendEmpty()
+	maximumLogRecord2[0].CopyTo(lr)
+	maximumLogRecord2[1].CopyTo(lr2)
+
+	expectedBadLevel := plog.NewLogs()
+	resourceLogs = expectedBadLevel.ResourceLogs().AppendEmpty()
+	scopeLogs = resourceLogs.ScopeLogs().AppendEmpty()
+	scopeLogs.Scope().SetName("otelcol/azureresourcelogs")
+	scopeLogs.Scope().SetVersion(testBuildInfo.Version)
+	lr = scopeLogs.LogRecords().AppendEmpty()
+	badLevelLogRecord.CopyTo(lr)
+
+	expectedBadTime := plog.NewLogs()
+	resourceLogs = expectedBadTime.ResourceLogs().AppendEmpty()
+	scopeLogs = resourceLogs.ScopeLogs().AppendEmpty()
+	scopeLogs.Scope().SetName("otelcol/azureresourcelogs")
+	scopeLogs.Scope().SetVersion(testBuildInfo.Version)
+	lr = scopeLogs.LogRecords().AppendEmpty()
+	badTimeLogRecord.CopyTo(lr)
+
+	tests := []struct {
+		file     string
+		expected plog.Logs
+	}{
+		{
+			file:     "log-minimum.json",
+			expected: expectedMinimum,
+		},
+		{
+			file:     "log-minimum-2.json",
+			expected: expectedMinimum2,
+		},
+		{
+			file:     "log-maximum.json",
+			expected: expectedMaximum,
+		},
+		{
+			file:     "log-bad-level.json",
+			expected: expectedBadLevel,
+		},
+		{
+			file:     "log-bad-time.json",
+			expected: expectedBadTime,
+		},
+	}
+
+	sut := &ResourceLogsUnmarshaler{
+		Version: testBuildInfo.Version,
+		Logger:  zap.NewNop(),
+	}
+	for _, tt := range tests {
+		t.Run(tt.file, func(t *testing.T) {
+			data, err := os.ReadFile(filepath.Join("testdata", tt.file))
+			assert.NoError(t, err)
+			assert.NotNil(t, data)
+
+			logs, err := sut.UnmarshalLogs(data)
+			assert.NoError(t, err)
+
+			assert.NoError(t, plogtest.CompareLogs(tt.expected, logs))
+		})
+	}
+}
+
+func loadJSONLogsAndApplySemanticConventions(filename string) (plog.Logs, error) {
+	l := plog.NewLogs()
+
+	sut := &ResourceLogsUnmarshaler{
+		Version: testBuildInfo.Version,
+		Logger:  zap.NewNop(),
+	}
+
+	data, err := os.ReadFile(filepath.Join("testdata", filename))
+	if err != nil {
+		return l, err
+	}
+
+	logs, err := sut.UnmarshalLogs(data)
+
+	if err != nil {
+		return l, err
+	}
+
+	return logs, nil
+}
+
+func TestAzureCdnAccessLog(t *testing.T) {
+	logs, err := loadJSONLogsAndApplySemanticConventions("log-azurecdnaccesslog.json")
+
+	assert.NoError(t, err)
+
+	record := logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Body().Map().AsRaw()
+
+	assert.Equal(t, "GET", record["http.request.method"])
+	assert.Equal(t, "1.1.0.0", record["network.protocol.version"])
+	assert.Equal(t, "TRACKING_REFERENCE", record["az.service_request_id"])
+	assert.Equal(t, "https://test.net/", record["url.full"])
+	assert.Equal(t, int64(1234), record["http.request.size"])
+	assert.Equal(t, int64(12345), record["http.response.size"])
+	assert.Equal(t, "Mozilla/5.0", record["user_agent.original"])
+	assert.Equal(t, "42.42.42.42", record["client.address"])
+	assert.Equal(t, "0", record["client.port"])
+	assert.Equal(t, "tls", record["tls.protocol.name"])
+	assert.Equal(t, "1.3", record["tls.protocol.version"])
+	assert.Equal(t, int64(200), record["http.response.status_code"])
+	assert.Equal(t, "NoError", record["error.type"])
+}
+
+func TestFrontDoorAccessLog(t *testing.T) {
+	logs, err := loadJSONLogsAndApplySemanticConventions("log-frontdooraccesslog.json")
+
+	assert.NoError(t, err)
+
+	record := logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Body().Map().AsRaw()
+
+	assert.Equal(t, "GET", record["http.request.method"])
+	assert.Equal(t, "1.1.0.0", record["network.protocol.version"])
+	assert.Equal(t, "TRACKING_REFERENCE", record["az.service_request_id"])
+	assert.Equal(t, "https://test.net/", record["url.full"])
+	assert.Equal(t, int64(1234), record["http.request.size"])
+	assert.Equal(t, int64(12345), record["http.response.size"])
+	assert.Equal(t, "Mozilla/5.0", record["user_agent.original"])
+	assert.Equal(t, "42.42.42.42", record["client.address"])
+	assert.Equal(t, "0", record["client.port"])
+	assert.Equal(t, "23.23.23.23", record["network.peer.address"])
+	assert.Equal(t, float64(0.23), record["http.server.request.duration"])
+	assert.Equal(t, "https", record["network.protocol.name"])
+	assert.Equal(t, "tls", record["tls.protocol.name"])
+	assert.Equal(t, "1.3", record["tls.protocol.version"])
+	assert.Equal(t, "TLS_AES_256_GCM_SHA384", record["tls.cipher"])
+	assert.Equal(t, "secp384r1", record["tls.curve"])
+	assert.Equal(t, int64(200), record["http.response.status_code"])
+	assert.Equal(t, "REFERER", record["http.request.header.referer"])
+	assert.Equal(t, "NoError", record["error.type"])
+}
+
+func TestFrontDoorHealthProbeLog(t *testing.T) {
+	logs, err := loadJSONLogsAndApplySemanticConventions("log-frontdoorhealthprobelog.json")
+
+	assert.NoError(t, err)
+
+	record := logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Body().Map().AsRaw()
+
+	assert.Equal(t, "GET", record["http.request.method"])
+	assert.Equal(t, int64(200), record["http.response.status_code"])
+	assert.Equal(t, "https://probe.net/health", record["url.full"])
+	assert.Equal(t, "42.42.42.42", record["server.address"])
+	assert.Equal(t, 0.042, record["http.request.duration"])
+	assert.Equal(t, 0.00023, record["dns.lookup.duration"])
+}
+
+func TestFrontDoorWAFLog(t *testing.T) {
+	logs, err := loadJSONLogsAndApplySemanticConventions("log-frontdoorwaflog.json")
+
+	assert.NoError(t, err)
+
+	record := logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Body().Map().AsRaw()
+
+	assert.Equal(t, "TRACKING_REFERENCE", record["az.service_request_id"])
+	assert.Equal(t, "https://test.net/", record["url.full"])
+	assert.Equal(t, "test.net", record["server.address"])
+	assert.Equal(t, "42.42.42.42", record["client.address"])
+	assert.Equal(t, "0", record["client.port"])
+	assert.Equal(t, "23.23.23.23", record["network.peer.address"])
+}
+
+func TestAppServiceAppLog(t *testing.T) {
+	logs, err := loadJSONLogsAndApplySemanticConventions("log-appserviceapplogs.json")
+
+	assert.NoError(t, err)
+
+	record := logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Body().Map().AsRaw()
+
+	assert.Equal(t, "CONTAINER_ID", record["container.id"])
+	assert.Equal(t, "EXCEPTION_CLASS", record["exception.type"])
+	assert.Equal(t, "HOST", record["host.id"])
+	assert.Equal(t, "METHOD", record["code.function"])
+	assert.Equal(t, "FILEPATH", record["code.filepath"])
+	assert.Equal(t, "STACKTRACE", record["exception.stacktrace"])
+}
+
+func TestAppServiceConsoleLog(t *testing.T) {
+	logs, err := loadJSONLogsAndApplySemanticConventions("log-appserviceconsolelogs.json")
+
+	assert.NoError(t, err)
+
+	record := logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Body().Map().AsRaw()
+
+	assert.Equal(t, "CONTAINER_ID", record["container.id"])
+	assert.Equal(t, "HOST", record["host.id"])
+}
+
+func TestAppServiceAuditLog(t *testing.T) {
+	logs, err := loadJSONLogsAndApplySemanticConventions("log-appserviceauditlogs.json")
+
+	assert.NoError(t, err)
+
+	record := logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Body().Map().AsRaw()
+
+	assert.Equal(t, "USER_ID", record["enduser.id"])
+	assert.Equal(t, "42.42.42.42", record["client.address"])
+	assert.Equal(t, "kudu", record["network.protocol.name"])
+}
+
+func TestAppServiceHTTPLog(t *testing.T) {
+	logs, err := loadJSONLogsAndApplySemanticConventions("log-appservicehttplogs.json")
+
+	assert.NoError(t, err)
+
+	record := logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Body().Map().AsRaw()
+
+	assert.Equal(t, "test.com", record["url.domain"])
+	assert.Equal(t, "42.42.42.42", record["client.address"])
+	assert.Equal(t, int64(80), record["server.port"])
+	assert.Equal(t, "/api/test/", record["url.path"])
+	assert.Equal(t, "foo=42", record["url.query"])
+	assert.Equal(t, "GET", record["http.request.method"])
+	assert.Equal(t, 0.42, record["http.server.request.duration"])
+	assert.Equal(t, int64(200), record["http.response.status_code"])
+	assert.Equal(t, int64(4242), record["http.request.body.size"])
+	assert.Equal(t, int64(42), record["http.response.body.size"])
+	assert.Equal(t, "Mozilla/5.0", record["user_agent.original"])
+	assert.Equal(t, "REFERER", record["http.request.header.referer"])
+	assert.Equal(t, "COMPUTER_NAME", record["host.name"])
+	assert.Equal(t, "http", record["network.protocol.name"])
+	assert.Equal(t, "1.1", record["network.protocol.version"])
+}
+
+func TestAppServicePlatformLog(t *testing.T) {
+	logs, err := loadJSONLogsAndApplySemanticConventions("log-appserviceplatformlogs.json")
+
+	assert.NoError(t, err)
+
+	record := logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Body().Map().AsRaw()
+
+	assert.Equal(t, "CONTAINER_ID", record["container.id"])
+	assert.Equal(t, "CONTAINER_NAME", record["container.name"])
+}
+
+func TestAppServiceIPSecAuditLog(t *testing.T) {
+	logs, err := loadJSONLogsAndApplySemanticConventions("log-appserviceipsecauditlogs.json")
+
+	assert.NoError(t, err)
+
+	record := logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).Body().Map().AsRaw()
+
+	assert.Equal(t, "42.42.42.42", record["client.address"])
+	assert.Equal(t, "HOST", record["url.domain"])
+	assert.Equal(t, "FDID", record["http.request.header.x-azure-fdid"])
+	assert.Equal(t, "HEALTH_PROBE", record["http.request.header.x-fd-healthprobe"])
+	assert.Equal(t, "FORWARDED_FOR", record["http.request.header.x-forwarded-for"])
+	assert.Equal(t, "FORWARDED_HOST", record["http.request.header.x-forwarded-host"])
+}
diff --git a/pkg/translator/azurelogs/testdata/log-appserviceapplogs.json b/pkg/translator/azurelogs/testdata/log-appserviceapplogs.json
new file mode 100644
index 000000000000..202a755a2286
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-appserviceapplogs.json
@@ -0,0 +1,18 @@
+{
+    "records": [
+        {
+            "time": "2024-04-24T12:06:12.0000000Z",
+            "resourceId": "/RESOURCE_ID",
+            "category": "AppServiceAppLogs",
+            "operationName": "AppLog",
+            "properties": {
+                "ContainerId": "CONTAINER_ID",
+                "ExceptionClass": "EXCEPTION_CLASS",
+                "Host": "HOST",
+                "Method": "METHOD",
+                "Source": "FILEPATH",
+                "Stacktrace": "STACKTRACE"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-appserviceauditlogs.json b/pkg/translator/azurelogs/testdata/log-appserviceauditlogs.json
new file mode 100644
index 000000000000..ba64020d5954
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-appserviceauditlogs.json
@@ -0,0 +1,16 @@
+{
+    "records": [
+        {
+            "time": "2024-04-24T12:01:20.8427400Z",
+            "ResourceId": "/SUBSCRIPTIONS/DA2DD5CC-E7BC-4DB6-94D9-0AFB3BD30577/RESOURCEGROUPS/FRETBADGER/PROVIDERS/MICROSOFT.WEB/SITES/FBEHTESTAPP",
+            "Category": "AppServiceAuditLogs",
+            "OperationName": "Authorization",
+            "Properties": {
+                "User": "USER_ID",
+                "UserDisplayName": "$fbehtestapp",
+                "UserAddress": "42.42.42.42",
+                "Protocol": "Kudu"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-appserviceconsolelogs.json b/pkg/translator/azurelogs/testdata/log-appserviceconsolelogs.json
new file mode 100644
index 000000000000..8158b619f8a8
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-appserviceconsolelogs.json
@@ -0,0 +1,14 @@
+{
+    "records": [
+        {
+            "time": "2024-04-24T12:06:12.0000000Z",
+            "resourceId": "/RESOURCE_ID",
+            "category": "AppServiceConsoleLogs",
+            "operationName": "ConsoleLog",
+            "properties": {
+                "ContainerId": "CONTAINER_ID",
+                "Host": "HOST"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-appservicehttplogs.json b/pkg/translator/azurelogs/testdata/log-appservicehttplogs.json
new file mode 100644
index 000000000000..6565dc976b3b
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-appservicehttplogs.json
@@ -0,0 +1,34 @@
+{
+    "records": [
+        {
+            "time": "2024-04-24T11:59:40.9893370Z",
+            "EventTime": "2024-04-24T11:59:40.9893370Z",
+            "resourceId": "/SUBSCRIPTIONS/DA2DD5CC-E7BC-4DB6-94D9-0AFB3BD30577/RESOURCEGROUPS/FRETBADGER/PROVIDERS/MICROSOFT.WEB/SITES/FBEHTESTAPP",
+            "properties": {
+                "CsHost": "test.com",
+                "CIp": "42.42.42.42",
+                "SPort": "80",
+                "CsUriStem": "/api/test/",
+                "CsUriQuery": "foo=42",
+                "CsMethod": "GET",
+                "TimeTaken": 420,
+                "ScStatus": "200",
+                "Result": "Success",
+                "CsBytes": "4242",
+                "ScBytes": "42",
+                "UserAgent": "Mozilla/5.0",
+                "Cookie": "",
+                "CsUsername": "user@test.com",
+                "Referer": "REFERER",
+                "ComputerName": "COMPUTER_NAME",
+                "Protocol": "HTTP/1.1"
+            },
+            "category": "AppServiceHTTPLogs",
+            "EventStampType": "Stamp",
+            "EventPrimaryStampName": "waws-prod-blu-479",
+            "EventStampName": "waws-prod-blu-479",
+            "Host": "lw0sdlwk0005XR",
+            "EventIpAddress": "10.50.0.34"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-appserviceipsecauditlogs.json b/pkg/translator/azurelogs/testdata/log-appserviceipsecauditlogs.json
new file mode 100644
index 000000000000..207d81ff629d
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-appserviceipsecauditlogs.json
@@ -0,0 +1,18 @@
+{
+    "records": [
+        {
+            "time": "2024-04-24T12:06:12.0000000Z",
+            "resourceId": "/RESOURCE_ID",
+            "category": "AppServiceIPSecAuditLogs",
+            "operationName": "IPSecAuditLog",
+            "properties": {
+                "CIp": "42.42.42.42",
+                "CsHost": "HOST",
+                "XAzureFDID": "FDID",
+                "XFDHealthProbe": "HEALTH_PROBE",
+                "XForwardedFor": "FORWARDED_FOR",
+                "XForwardedHost": "FORWARDED_HOST"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-appserviceplatformlogs.json b/pkg/translator/azurelogs/testdata/log-appserviceplatformlogs.json
new file mode 100644
index 000000000000..903e5dcfcd25
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-appserviceplatformlogs.json
@@ -0,0 +1,21 @@
+{
+    "records": [
+        {
+            "resourceId": "/SUBSCRIPTIONS/DA2DD5CC-E7BC-4DB6-94D9-0AFB3BD30577/RESOURCEGROUPS/FRETBADGER/PROVIDERS/MICROSOFT.WEB/SITES/FBEHTESTAPP",
+            "category": "AppServicePlatformLogs",
+            "time": "2024-04-24T12:03:55.630Z",
+            "level": "Informational",
+            "operationName": "ContainerLogs",
+            "properties": {
+                "message": "Initiating warmup request to container fbehtestapp_1_b8a27b37 for site fbehtestapp",
+                "containerId": "CONTAINER_ID",
+                "containerName": "CONTAINER_NAME"
+            },
+            "EventStampType": "Stamp",
+            "EventPrimaryStampName": "waws-prod-blu-479",
+            "EventStampName": "waws-prod-blu-479",
+            "Host": "lw0sdlwk0005XR",
+            "EventIpAddress": "10.50.0.34"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-azurecdnaccesslog.json b/pkg/translator/azurelogs/testdata/log-azurecdnaccesslog.json
new file mode 100644
index 000000000000..2a905fb85e16
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-azurecdnaccesslog.json
@@ -0,0 +1,34 @@
+{
+  "records": [
+    {
+      "time": "2024-04-24T12:06:12.0000000Z",
+      "resourceId": "/RESOURCE_ID",
+      "category": "AzureCdnAccessLog",
+      "operationName": "Microsoft.AzureCdn/Profiles/AccessLog",
+      "properties": {
+        "BackendHostName": "backendhost.net",
+        "ClientIp": "42.42.42.42",
+        "ClientPort": "0",
+        "HttpMethod": "GET",
+        "HttpVersion": "1.1.0.0",
+        "HttpStatusCode": "200",
+        "HttpStatusDetails": "200",
+        "POP": "LON",
+        "RequestBytes": "1234",
+        "RequestUri": "https://test.net/",
+        "ResponseBytes": "12345",
+        "RoutingRuleName": "default-route",
+        "RulesEngineMatchNames": [],
+        "SecurityProtocol": "TLS 1.3",
+        "isReceivedFromClient": false,
+        "TimeTaken": "0.230",
+        "TrackingReference": "TRACKING_REFERENCE",
+        "UserAgent": "Mozilla/5.0",
+        "ErrorInfo": "NoError",
+        "TimeToFirstByte": "0.420",
+        "Result": "N/A",
+        "SNI": "originshield|parentcache|https|tier2"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-bad-level.json b/pkg/translator/azurelogs/testdata/log-bad-level.json
new file mode 100644
index 000000000000..662d34821f28
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-bad-level.json
@@ -0,0 +1,39 @@
+{
+    "records": [
+        {
+            "DeploymentVersion": "0.40.16708.0",
+            "Level": 4,
+            "isRequestSuccess": true,
+            "time": "2023-10-26T14:22:43.3416357Z",
+            "operationName": "Microsoft.ApiManagement/GatewayLogs",
+            "category": "GatewayLogs",
+            "durationMs": 243,
+            "callerIpAddress": "13.14.15.16",
+            "correlationId": "128bc026-5ead-40c7-8853-ebb32bc077a3",
+            "location": "West US",
+            "properties": {
+                "method": "GET",
+                "url": "https://api.azure-api.net/sessions",
+                "backendResponseCode": 200,
+                "responseCode": 200,
+                "responseSize": 102945,
+                "cache": "none",
+                "backendTime": 54,
+                "requestSize": 632,
+                "apiId": "demo-api",
+                "operationId": "GetSessions",
+                "apimSubscriptionId": "master",
+                "clientTime": 190,
+                "clientProtocol": "HTTP/1.1",
+                "backendProtocol": "HTTP/1.1",
+                "apiRevision": "1",
+                "clientTlsVersion": "1.2",
+                "backendMethod": "GET",
+                "backendUrl": "https://api.azurewebsites.net/sessions"
+            },
+            "resourceId": "/RESOURCE_ID",
+            "resultType": "Succeeded",
+            "truncated": 0
+        }
+    ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-bad-time.json b/pkg/translator/azurelogs/testdata/log-bad-time.json
new file mode 100644
index 000000000000..614d170378ec
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-bad-time.json
@@ -0,0 +1,45 @@
+{
+    "records": [
+        {
+            "timeStamp": "2021-10-14T22:17:11+00:00",
+            "resourceId": "/RESOURCE_ID",
+            "listenerName": "HTTP-Listener",
+            "ruleName": "Storage-Static-Rule",
+            "backendPoolName": "StaticStorageAccount",
+            "backendSettingName": "StorageStatic-HTTPS-Setting",
+            "operationName": "ApplicationGatewayAccess",
+            "category": "ApplicationGatewayAccessLog",
+            "properties": {
+                "instanceId": "appgw_2",
+                "clientIP": "185.42.129.24",
+                "clientPort": 45057,
+                "httpMethod": "GET",
+                "originalRequestUriWithArgs": "\/",
+                "requestUri": "\/",
+                "requestQuery": "",
+                "userAgent": "Mozilla\/5.0 (Windows NT 6.1; WOW64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/52.0.2743.116 Safari\/537.36",
+                "httpStatus": 200,
+                "httpVersion": "HTTP\/1.1",
+                "receivedBytes": 184,
+                "sentBytes": 466,
+                "clientResponseTime": 0,
+                "timeTaken": 0.034,
+                "WAFEvaluationTime": "0.000",
+                "WAFMode": "Detection",
+                "transactionId": "592d1649f75a8d480a3c4dc6a975309d",
+                "sslEnabled": "on",
+                "sslCipher": "ECDHE-RSA-AES256-GCM-SHA384",
+                "sslProtocol": "TLSv1.2",
+                "sslClientVerify": "NONE",
+                "sslClientCertificateFingerprint": "",
+                "sslClientCertificateIssuerName": "",
+                "serverRouted": "52.239.221.65:443",
+                "serverStatus": "200",
+                "serverResponseLatency": "0.028",
+                "upstreamSourcePort": "21564",
+                "originalHost": "20.110.30.194",
+                "host": "20.110.30.194"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-frontdooraccesslog.json b/pkg/translator/azurelogs/testdata/log-frontdooraccesslog.json
new file mode 100644
index 000000000000..0a775bdb9572
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-frontdooraccesslog.json
@@ -0,0 +1,46 @@
+{
+  "records": [
+    {
+      "time": "2024-04-24T12:06:12.0000000Z",
+      "resourceId": "/RESOURCE_ID",
+      "category": "FrontDoorAccessLog",
+      "operationName": "Microsoft.Cdn/Profiles/AccessLog/Write",
+      "properties": {
+        "trackingReference": "TRACKING_REFERENCE",
+        "httpMethod": "GET",
+        "httpVersion": "1.1.0.0",
+        "requestUri": "https://test.net/",
+        "sni": "originshield|parentcache|https|tier2",
+        "requestBytes": "1234",
+        "responseBytes": "12345",
+        "userAgent": "Mozilla/5.0",
+        "clientIp": "42.42.42.42",
+        "clientPort": "0",
+        "socketIp": "23.23.23.23",
+        "timeToFirstByte": "0.420",
+        "timeTaken": "0.230",
+        "requestProtocol": "HTTPS",
+        "securityProtocol": "TLS 1.3",
+        "rulesEngineMatchNames": [],
+        "httpStatusCode": "200",
+        "httpStatusDetails": "200",
+        "pop": "LON",
+        "cacheStatus": "MISS",
+        "errorInfo": "NoError",
+        "ErrorInfo": "NoError",
+        "result": "N/A",
+        "endpoint": "dummyapp-eebde0bwehfthfbb.z01.azurefd.net",
+        "routingRuleName": "default-route",
+        "hostName": "dummyapp-eebde0bwehfthfbb.z01.azurefd.net",
+        "originUrl": "https://dummyapp.icysea-e3cd8b77.eastus.azurecontainerapps.io:443/",
+        "originIp": "4.156.233.180:443",
+        "originName": "dummyapp.icysea-e3cd8b77.eastus.azurecontainerapps.io:443",
+        "referer": "REFERER",
+        "clientCountry": "United Kingdom",
+        "domain": "dummyapp-eebde0bwehfthfbb.z01.azurefd.net:443",
+        "securityCipher": "TLS_AES_256_GCM_SHA384",
+        "securityCurves": "secp384r1"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-frontdoorhealthprobelog.json b/pkg/translator/azurelogs/testdata/log-frontdoorhealthprobelog.json
new file mode 100644
index 000000000000..f0cf4bb0560e
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-frontdoorhealthprobelog.json
@@ -0,0 +1,22 @@
+{
+  "records": [
+    {
+      "time": "2024-04-24T12:06:12.0000000Z",
+      "resourceId": "/RESOURCE_ID",
+      "category": "FrontDoorHealthProbeLog",
+      "operationName": "WAF/FirewallLog",
+      "properties": {
+        "healthProbeId": "AAAA",
+        "POP": "",
+        "httpVerb": "GET",
+        "result": "",
+        "httpStatusCode": "200",
+        "probeURL": "https://probe.net/health",
+        "originName": "https://probe.net/",
+        "originIP": "42.42.42.42",
+        "totalLatencyMilliseconds": "42",
+        "DNSLatencyMicroseconds": "230"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-frontdoorwaflog.json b/pkg/translator/azurelogs/testdata/log-frontdoorwaflog.json
new file mode 100644
index 000000000000..6a894fe955a7
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-frontdoorwaflog.json
@@ -0,0 +1,18 @@
+{
+  "records": [
+    {
+      "time": "2024-04-24T12:06:12.0000000Z",
+      "resourceId": "/RESOURCE_ID",
+      "category": "FrontdoorWebApplicationFirewallLog",
+      "operationName": "WAF/FirewallLog",
+      "properties": {
+        "trackingReference": "TRACKING_REFERENCE",
+        "clientIP": "42.42.42.42",
+        "clientPort": "0",
+        "socketIP": "23.23.23.23",
+        "requestUri": "https://test.net/",
+        "host": "test.net"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/pkg/translator/azurelogs/testdata/log-maximum.json b/pkg/translator/azurelogs/testdata/log-maximum.json
new file mode 100644
index 000000000000..b105c6b168bc
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-maximum.json
@@ -0,0 +1,85 @@
+{
+  "records": [
+    {
+      "time": "2022-11-11T04:48:27.6767145Z",
+      "resourceId": "/RESOURCE_ID-1",
+      "tenantId": "/TENANT_ID",
+      "operationName": "SecretGet",
+      "operationVersion": "7.0",
+      "category": "AuditEvent",
+      "resultType": "Success",
+      "resultSignature": "Signature",
+      "resultDescription": "Description",
+      "durationMs": "1234",
+      "callerIpAddress": "127.0.0.1",
+      "correlationId": "607964b6-41a5-4e24-a5db-db7aab3b9b34",
+      "Level": "Warning",
+      "location": "ukso",
+      "identity": {
+        "claim": {
+          "oid": "607964b6-41a5-4e24-a5db-db7aab3b9b34"
+        }
+      },
+      "properties": {
+        "string": "string",
+        "int": 429,
+        "float": 3.14,
+        "bool": false
+      }
+    },
+    {
+      "time": "2022-11-11T04:48:29.6767145Z",
+      "resourceId": "/RESOURCE_ID-2",
+      "tenantId": "/TENANT_ID",
+      "operationName": "SecretSet",
+      "operationVersion": "7.0",
+      "category": "AuditEvent",
+      "resultType": "Success",
+      "resultSignature": "Signature",
+      "resultDescription": "Description",
+      "durationMs": "4321",
+      "callerIpAddress": "127.0.0.1",
+      "correlationId": "96317703-2132-4a8d-a5d7-e18d2f486783",
+      "Level": "Warning",
+      "location": "ukso",
+      "identity": {
+        "claim": {
+          "oid": "96317703-2132-4a8d-a5d7-e18d2f486783"
+        }
+      },
+      "properties": {
+        "string": "string",
+        "int": 924,
+        "float": 41.3,
+        "bool": true
+      }
+    },
+    {
+      "time": "2022-11-11T04:48:31.6767145Z",
+      "resourceId": "/RESOURCE_ID-2",
+      "tenantId": "/TENANT_ID",
+      "operationName": "SecretGet",
+      "operationVersion": "7.0",
+      "category": "AuditEvent",
+      "resultType": "Success",
+      "resultSignature": "Signature",
+      "resultDescription": "Description",
+      "durationMs": "321",
+      "callerIpAddress": "127.0.0.1",
+      "correlationId": "4ae807da-39d9-4327-b5b4-0ab685a57f9a",
+      "Level": "Warning",
+      "location": "ukso",
+      "identity": {
+        "claim": {
+          "oid": "4ae807da-39d9-4327-b5b4-0ab685a57f9a"
+        }
+      },
+      "properties": {
+        "string": "string",
+        "int": 925,
+        "float": 41.4,
+        "bool": false
+      }
+    }
+  ]
+}
diff --git a/pkg/translator/azurelogs/testdata/log-minimum-2.json b/pkg/translator/azurelogs/testdata/log-minimum-2.json
new file mode 100644
index 000000000000..6eac63fa0389
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-minimum-2.json
@@ -0,0 +1,16 @@
+{
+  "records": [
+    {
+      "time": "2022-11-11T04:48:27.6767145Z",
+      "resourceId": "/RESOURCE_ID",
+      "operationName": "SecretGet",
+      "category": "AuditEvent"
+    },
+    {
+      "time": "2022-11-11T04:48:27.6767145Z",
+      "resourceId": "/RESOURCE_ID",
+      "operationName": "SecretGet",
+      "category": "AuditEvent"
+    }
+  ]
+}
diff --git a/pkg/translator/azurelogs/testdata/log-minimum.json b/pkg/translator/azurelogs/testdata/log-minimum.json
new file mode 100644
index 000000000000..16d4f2e71177
--- /dev/null
+++ b/pkg/translator/azurelogs/testdata/log-minimum.json
@@ -0,0 +1,10 @@
+{
+  "records": [
+    {
+      "time": "2022-11-11T04:48:27.6767145Z",
+      "resourceId": "/RESOURCE_ID",
+      "operationName": "SecretGet",
+      "category": "AuditEvent"
+    }
+  ]
+}
diff --git a/versions.yaml b/versions.yaml
index c63af9fe4b11..3a5e59023027 100644
--- a/versions.yaml
+++ b/versions.yaml
@@ -151,6 +151,7 @@ module-sets:
       - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza
       - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/datadog
       - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azure
+      - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/azurelogs
       - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger
       - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/loki
       - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/opencensus