diff --git a/config/feature_flags/wip/observability_logs.yml b/config/feature_flags/wip/observability_logs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8b262d268b37d983df69d577607b00a46b2c6721
--- /dev/null
+++ b/config/feature_flags/wip/observability_logs.yml
@@ -0,0 +1,8 @@
+---
+name: observability_logs
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142989
+rollout_issue_url: https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2643
+milestone: '16.9'
+type: wip
+group: group::observability
+default_enabled: false
diff --git a/ee/app/controllers/projects/logs_controller.rb b/ee/app/controllers/projects/logs_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5370bf695c7735cc9e87f2f0574970c6544bad99
--- /dev/null
+++ b/ee/app/controllers/projects/logs_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Projects
+  class LogsController < Projects::ApplicationController
+    include ::Observability::ContentSecurityPolicy
+
+    feature_category :metrics
+
+    before_action :authorize_read_observability_logs!
+
+    def index; end
+  end
+end
diff --git a/ee/app/helpers/projects/observability_helper.rb b/ee/app/helpers/projects/observability_helper.rb
index 83390863ce04e2708a8016957bc27d15f6793a61..bc0a991f52f9f96b93cef675cd6e6f98d4792c5d 100644
--- a/ee/app/helpers/projects/observability_helper.rb
+++ b/ee/app/helpers/projects/observability_helper.rb
@@ -25,6 +25,10 @@ def observability_tracing_details_model(project, trace_id)
       end
     end
 
+    def observability_logs_view_model(project)
+      generate_model(project)
+    end
+
     private
 
     def generate_model(project)
@@ -46,7 +50,8 @@ def shared_model(project)
           operationsUrl: ::Gitlab::Observability.operations_url(project),
           metricsUrl: ::Gitlab::Observability.metrics_url(project),
           metricsSearchUrl: ::Gitlab::Observability.metrics_search_url(project),
-          metricsSearchMetadataUrl: ::Gitlab::Observability.metrics_search_metadata_url(project)
+          metricsSearchMetadataUrl: ::Gitlab::Observability.metrics_search_metadata_url(project),
+          logsSearchUrl: ::Gitlab::Observability.logs_search_url(project)
         }
       }
     end
diff --git a/ee/app/models/gitlab_subscriptions/features.rb b/ee/app/models/gitlab_subscriptions/features.rb
index 56a9c9fae171016156007e24cfe3b31adef547ee..583ee36c96dce433a910e81722f64b99f8685603 100644
--- a/ee/app/models/gitlab_subscriptions/features.rb
+++ b/ee/app/models/gitlab_subscriptions/features.rb
@@ -128,6 +128,7 @@ class Features
       merge_trains
       metrics_reports
       metrics_observability
+      logs_observability
       multiple_alert_http_integrations
       multiple_approval_rules
       multiple_group_issue_boards
diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb
index 8ea26d93947a6e507ba149407c047c4460eb1052..fae93bd1f73eedcf087351631377921105e26114 100644
--- a/ee/app/policies/ee/project_policy.rb
+++ b/ee/app/policies/ee/project_policy.rb
@@ -348,6 +348,11 @@ module ProjectPolicy
           @subject.licensed_feature_available?(:metrics_observability)
       end
 
+      condition(:observability_logs_enabled) do
+        ::Feature.enabled?(:observability_logs, @subject.root_namespace, type: :wip) &&
+          @subject.licensed_feature_available?(:logs_observability)
+      end
+
       # We are overriding the already defined condition in CE version
       # to allow Guest users with member roles to access the merge requests.
       condition(:merge_requests_disabled) do
@@ -871,6 +876,10 @@ module ProjectPolicy
         enable :read_observability_metrics
       end
 
+      rule { can?(:reporter_access) & observability_logs_enabled }.policy do
+        enable :read_observability_logs
+      end
+
       rule { ci_cancellation_maintainers_only & ~can?(:maintainer_access) }.policy do
         prevent :cancel_pipeline
         prevent :cancel_build
diff --git a/ee/app/views/projects/logs/index.html.haml b/ee/app/views/projects/logs/index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..2da9001225f7c8fa201396d0e4d7e0abc0618f9c
--- /dev/null
+++ b/ee/app/views/projects/logs/index.html.haml
@@ -0,0 +1,5 @@
+- page_title s_('ObservabilityLogs|Logs')
+- @content_wrapper_class = "#{@content_wrapper_class} gl-pb-0"
+- @no_container = true
+
+#js-observability-logs{ data: { view_model: observability_logs_view_model(@project) } }
diff --git a/ee/config/routes/project.rb b/ee/config/routes/project.rb
index 3bc123425fb63275ad0f552a35ea21cdedcb39e1..7620d24eb335858131180758c3e181712c90a7d4 100644
--- a/ee/config/routes/project.rb
+++ b/ee/config/routes/project.rb
@@ -153,6 +153,8 @@
 
         resources :metrics, only: [:index, :show], constraints: { id: %r{[^/]+}, type: /\w+/ }, controller: :metrics
 
+        resources :logs, only: [:index], controller: :logs
+
         namespace :ml do
           resources :agents, only: [:index, :new, :show], controller: 'agents', param: :agent_id
         end
diff --git a/ee/lib/ee/gitlab/observability.rb b/ee/lib/ee/gitlab/observability.rb
index 769f0df7fb2308116864fcc5883379d36e367938..b4a943e9560759cdfa8ef0801bb86e16d21ad55a 100644
--- a/ee/lib/ee/gitlab/observability.rb
+++ b/ee/lib/ee/gitlab/observability.rb
@@ -34,6 +34,10 @@ def metrics_search_url(project)
         def metrics_search_metadata_url(project)
           "#{::Gitlab::Observability.observability_url}/v3/query/#{project.id}/metrics/searchmetadata"
         end
+
+        def logs_search_url(project)
+          "#{::Gitlab::Observability.observability_url}/v3/query/#{project.id}/logs/search"
+        end
       end
     end
   end
diff --git a/ee/lib/ee/sidebars/projects/menus/monitor_menu.rb b/ee/lib/ee/sidebars/projects/menus/monitor_menu.rb
index 7f3efd5668a231a768c651a35dc1325b9426bd1c..686f8339829b81196c24ddf5bee69856650183b8 100644
--- a/ee/lib/ee/sidebars/projects/menus/monitor_menu.rb
+++ b/ee/lib/ee/sidebars/projects/menus/monitor_menu.rb
@@ -13,6 +13,7 @@ def configure_menu_items
 
             insert_item_before(:error_tracking, tracing_menu_item)
             insert_item_after(:tracing, metrics_menu_item)
+            insert_item_after(:logs, logs_menu_item)
             insert_item_after(:incidents, on_call_schedules_menu_item)
             insert_item_after(:on_call_schedules, escalation_policies_menu_item)
 
@@ -76,6 +77,20 @@ def metrics_menu_item
               item_id: :metrics
             )
           end
+
+          def logs_menu_item
+            unless can?(context.current_user, :read_observability_logs, context.project)
+              return ::Sidebars::NilMenuItem.new(item_id: :logs)
+            end
+
+            ::Sidebars::MenuItem.new(
+              title: s_('ObservabilityLogs|Logs'),
+              link: project_logs_path(context.project),
+              super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::MonitorMenu,
+              active_routes: { controller: :logs },
+              item_id: :logs
+            )
+          end
         end
       end
     end
diff --git a/ee/spec/helpers/projects/observability_helper_spec.rb b/ee/spec/helpers/projects/observability_helper_spec.rb
index 1fdeb4952b77a48d773a29e7294bde2e05eb182c..69d905411b28943d0d32f6b141264bac39ea73f0 100644
--- a/ee/spec/helpers/projects/observability_helper_spec.rb
+++ b/ee/spec/helpers/projects/observability_helper_spec.rb
@@ -19,7 +19,8 @@
       operationsUrl: Gitlab::Observability.operations_url(project),
       metricsUrl: Gitlab::Observability.metrics_url(project),
       metricsSearchUrl: Gitlab::Observability.metrics_search_url(project),
-      metricsSearchMetadataUrl: Gitlab::Observability.metrics_search_metadata_url(project)
+      metricsSearchMetadataUrl: Gitlab::Observability.metrics_search_metadata_url(project),
+      logsSearchUrl: Gitlab::Observability.logs_search_url(project)
     }
   end
 
@@ -68,4 +69,14 @@
         .to eq(expected_json)
     end
   end
+
+  describe '#observability_logs_view_model' do
+    it 'generates the correct JSON' do
+      expected_json = {
+        apiConfig: expected_api_config
+      }.to_json
+
+      expect(helper.observability_logs_view_model(project)).to eq(expected_json)
+    end
+  end
 end
diff --git a/ee/spec/lib/ee/gitlab/observability_spec.rb b/ee/spec/lib/ee/gitlab/observability_spec.rb
index 1998614d0eea53b257ead6f234d2169167256f54..a4d5a7a2d8fcaac9d5a1db8b6811a4ebe13c41bd 100644
--- a/ee/spec/lib/ee/gitlab/observability_spec.rb
+++ b/ee/spec/lib/ee/gitlab/observability_spec.rb
@@ -51,4 +51,10 @@
 
     it { is_expected.to eq("#{described_class.observability_url}/v3/query/#{project.id}/metrics/searchmetadata") }
   end
+
+  describe '.logs_search_url' do
+    subject { described_class.logs_search_url(project) }
+
+    it { is_expected.to eq("#{described_class.observability_url}/v3/query/#{project.id}/logs/search") }
+  end
 end
diff --git a/ee/spec/lib/ee/sidebars/projects/menus/monitor_menu_spec.rb b/ee/spec/lib/ee/sidebars/projects/menus/monitor_menu_spec.rb
index 38502d778903ba9897b1107b549a28673ae33b96..af8000c6dd41e1a231741f0f875b29b41ec401c5 100644
--- a/ee/spec/lib/ee/sidebars/projects/menus/monitor_menu_spec.rb
+++ b/ee/spec/lib/ee/sidebars/projects/menus/monitor_menu_spec.rb
@@ -111,5 +111,40 @@
         it { is_expected.to be_nil }
       end
     end
+
+    describe 'Logs', feature_category: :metrics do
+      let(:item_id) { :logs }
+      let(:user) { build(:user) }
+      let(:role) { :reporter }
+
+      before do
+        stub_licensed_features(logs_observability: true)
+        stub_member_access_level(project, role => user)
+      end
+
+      it { is_expected.not_to be_nil }
+
+      describe 'when feature flag is disabled' do
+        before do
+          stub_feature_flags(observability_logs: false)
+        end
+
+        it { is_expected.to be_nil }
+      end
+
+      describe 'when unlicensed' do
+        before do
+          stub_licensed_features(logs_observability: false)
+        end
+
+        it { is_expected.to be_nil }
+      end
+
+      describe 'when user does not have permissions' do
+        let(:role) { :guest }
+
+        it { is_expected.to be_nil }
+      end
+    end
   end
 end
diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb
index d63536b8984e28b1c93fd65df355e9fcb1c44adf..4ad932606e9f430563ef1ed552c5151f52516ba6 100644
--- a/ee/spec/policies/project_policy_spec.rb
+++ b/ee/spec/policies/project_policy_spec.rb
@@ -3242,6 +3242,49 @@ def create_member_role(member, abilities = member_role_abilities)
     end
   end
 
+  describe 'read_observability_logs policy' do
+    let(:current_user) { reporter }
+
+    before do
+      stub_licensed_features(logs_observability: true)
+    end
+
+    describe 'when feature flag is disabled' do
+      before do
+        stub_feature_flags(observability_logs: false)
+      end
+
+      it { is_expected.to be_disallowed(:read_observability_logs) }
+    end
+
+    describe 'when feature flag is enabled for root namespace' do
+      before do
+        stub_feature_flags(observability_logs: false)
+        stub_feature_flags(observability_logs: project.root_namespace)
+      end
+
+      it { is_expected.to be_allowed(:read_observability_logs) }
+    end
+
+    describe 'when the project does not have the correct license' do
+      before do
+        stub_licensed_features(logs_observability: false)
+      end
+
+      it { is_expected.to be_disallowed(:read_observability_logs) }
+    end
+
+    describe 'when the user does not have permission' do
+      let(:current_user) { guest }
+
+      it { is_expected.to be_disallowed(:read_observability_logs) }
+    end
+
+    describe 'when the user has permission' do
+      it { is_expected.to be_allowed(:read_observability_logs) }
+    end
+  end
+
   describe 'generate_cube_query policy' do
     using RSpec::Parameterized::TableSyntax
 
diff --git a/ee/spec/requests/projects/logs_controller_spec.rb b/ee/spec/requests/projects/logs_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..29b68a4090cd78821731b021dcbead9113569242
--- /dev/null
+++ b/ee/spec/requests/projects/logs_controller_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::LogsController, feature_category: :metrics do
+  let_it_be(:group) { create(:group) }
+  let_it_be(:project) { create(:project, group: group) }
+  let_it_be(:user) { create(:user) }
+  let(:path) { nil }
+  let(:observability_logs_ff) { true }
+  let(:expected_api_config) do
+    {
+      oauthUrl: Gitlab::Observability.oauth_url,
+      provisioningUrl: Gitlab::Observability.provisioning_url(project),
+      tracingUrl: Gitlab::Observability.tracing_url(project),
+      tracingAnalyticsUrl: Gitlab::Observability.tracing_analytics_url(project),
+      servicesUrl: Gitlab::Observability.services_url(project),
+      operationsUrl: Gitlab::Observability.operations_url(project),
+      metricsUrl: Gitlab::Observability.metrics_url(project),
+      metricsSearchUrl: Gitlab::Observability.metrics_search_url(project),
+      metricsSearchMetadataUrl: Gitlab::Observability.metrics_search_metadata_url(project),
+      logsSearchUrl: Gitlab::Observability.logs_search_url(project)
+    }
+  end
+
+  subject(:html_response) do
+    get path
+    response
+  end
+
+  before do
+    stub_licensed_features(logs_observability: true)
+    stub_feature_flags(observability_logs: observability_logs_ff)
+    sign_in(user)
+  end
+
+  shared_examples 'logs route request' do
+    it_behaves_like 'observability csp policy' do
+      before_all do
+        project.add_reporter(user)
+      end
+
+      let(:tested_path) { path }
+    end
+
+    context 'when user does not have permissions' do
+      before_all do
+        project.add_guest(user)
+      end
+
+      it 'returns 404' do
+        expect(html_response).to have_gitlab_http_status(:not_found)
+      end
+    end
+
+    context 'when user has permissions' do
+      before_all do
+        project.add_reporter(user)
+      end
+
+      it 'returns 200' do
+        expect(html_response).to have_gitlab_http_status(:ok)
+      end
+
+      context 'when feature is disabled' do
+        let(:observability_logs_ff) { false }
+
+        it 'returns 404' do
+          expect(html_response).to have_gitlab_http_status(:not_found)
+        end
+      end
+    end
+  end
+
+  describe 'GET #index' do
+    let(:path) { project_logs_path(project) }
+
+    it_behaves_like 'logs route request'
+
+    describe 'html response' do
+      before_all do
+        project.add_reporter(user)
+      end
+
+      it 'renders the js-logs element correctly' do
+        element = Nokogiri::HTML.parse(html_response.body).at_css('#js-observability-logs')
+
+        expected_view_model = {
+          apiConfig: expected_api_config
+        }.to_json
+        expect(element.attributes['data-view-model'].value).to eq(expected_view_model)
+      end
+    end
+  end
+end
diff --git a/ee/spec/requests/projects/metrics_controller_spec.rb b/ee/spec/requests/projects/metrics_controller_spec.rb
index 6e92433085bb93b341a58e921dca53751618d36a..44a6d8338a689d6726f3a6903804bb2f29e0c350 100644
--- a/ee/spec/requests/projects/metrics_controller_spec.rb
+++ b/ee/spec/requests/projects/metrics_controller_spec.rb
@@ -18,7 +18,8 @@
       operationsUrl: Gitlab::Observability.operations_url(project),
       metricsUrl: Gitlab::Observability.metrics_url(project),
       metricsSearchUrl: Gitlab::Observability.metrics_search_url(project),
-      metricsSearchMetadataUrl: Gitlab::Observability.metrics_search_metadata_url(project)
+      metricsSearchMetadataUrl: Gitlab::Observability.metrics_search_metadata_url(project),
+      logsSearchUrl: Gitlab::Observability.logs_search_url(project)
     }
   end
 
diff --git a/ee/spec/requests/projects/tracing_controller_spec.rb b/ee/spec/requests/projects/tracing_controller_spec.rb
index 383d4794516ab7a595fb71b4aa4100915d94f1da..33a12250325b3a66ce9d2d830af3f08403b5e1b2 100644
--- a/ee/spec/requests/projects/tracing_controller_spec.rb
+++ b/ee/spec/requests/projects/tracing_controller_spec.rb
@@ -18,7 +18,8 @@
       operationsUrl: Gitlab::Observability.operations_url(project),
       metricsUrl: Gitlab::Observability.metrics_url(project),
       metricsSearchUrl: Gitlab::Observability.metrics_search_url(project),
-      metricsSearchMetadataUrl: Gitlab::Observability.metrics_search_metadata_url(project)
+      metricsSearchMetadataUrl: Gitlab::Observability.metrics_search_metadata_url(project),
+      logsSearchUrl: Gitlab::Observability.logs_search_url(project)
     }
   end
 
diff --git a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
index d4ecf132c4444b07eae44bd8d137ff3c203f9a88..6cdfa7d9c376849fd39d1259a23ae04b1f4e5f21 100644
--- a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
+++ b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
@@ -19,6 +19,7 @@ def configure_menu_items
           [
             :tracing,
             :metrics,
+            :logs,
             :error_tracking,
             :alert_management,
             :incidents,
diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
index 3f8a146f040ee5d8dfa92add372806b2814ccca9..5cf9353363994ba55b91df2b8c59cce75f4ad0ed 100644
--- a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
+++ b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
@@ -17,6 +17,7 @@
     expect(items.map(&:item_id)).to eq([
       :tracing,
       :metrics,
+      :logs,
       :error_tracking,
       :alert_management,
       :incidents,
diff --git a/spec/support/helpers/user_with_namespace_shim.yml b/spec/support/helpers/user_with_namespace_shim.yml
index 9b546c5709d0fb1e25e153528b616e9896d7cb8d..f91f8492c8fd819c451b3397a693606d03c49551 100644
--- a/spec/support/helpers/user_with_namespace_shim.yml
+++ b/spec/support/helpers/user_with_namespace_shim.yml
@@ -233,6 +233,7 @@
 - ee/spec/requests/projects/analytics/code_reviews_controller_spec.rb
 - ee/spec/requests/projects/issues_controller_spec.rb
 - ee/spec/requests/projects/metrics_controller_spec.rb
+- ee/spec/requests/projects/logs_controller_spec.rb
 - ee/spec/requests/projects/security/policies_controller_spec.rb
 - ee/spec/requests/projects/tracing_controller_spec.rb
 - ee/spec/requests/subscriptions/hand_raise_leads_spec.rb