usage_data.rb 19.0 KB
Newer Older
G
gfyoung 已提交
1 2
# frozen_string_literal: true

3 4 5
# For hardening usage ping and make it easier to add measures there is in place
#   * alt_usage_data method
#     handles StandardError and fallbacks into -1 this way not all measures fail if we encounter one exception
6
#
7 8 9 10 11 12 13 14 15 16 17 18
#     Examples:
#     alt_usage_data { Gitlab::VERSION }
#     alt_usage_data { Gitlab::CurrentSettings.uuid }
#
#   * redis_usage_data method
#     handles ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
#     returns -1 when a block is sent or hash with all values -1 when a counter is sent
#     different behaviour due to 2 different implementations of redis counter
#
#     Examples:
#     redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter)
#     redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
19 20
module Gitlab
  class UsageData
21
    BATCH_SIZE = 100
22
    FALLBACK = -1
23

24
    class << self
25
      def data(force_refresh: false)
A
Alex Kalderimis 已提交
26 27 28
        Rails.cache.fetch('usage_data', force: force_refresh, expires_in: 2.weeks) do
          uncached_data
        end
29 30 31
      end

      def uncached_data
32 33
        license_usage_data
          .merge(system_usage_data)
34 35 36
          .merge(features_usage_data)
          .merge(components_usage_data)
          .merge(cycle_analytics_usage_data)
37
          .merge(object_store_usage_data)
38
          .merge(recording_ce_finish_data)
39 40
      end

41 42
      def to_json(force_refresh: false)
        data(force_refresh: force_refresh).to_json
43 44
      end

45
      def license_usage_data
46
        {
47
          recorded_at: Time.now, # should be calculated very first
48 49 50 51
          uuid: alt_usage_data { Gitlab::CurrentSettings.uuid },
          hostname: alt_usage_data { Gitlab.config.gitlab.host },
          version: alt_usage_data { Gitlab::VERSION },
          installation_type: alt_usage_data { installation_type },
52
          active_user_count: count(User.active),
53 54 55 56
          edition: 'CE'
        }
      end

57 58 59 60 61 62
      def recording_ce_finish_data
        {
          recording_ce_finished_at: Time.now
        }
      end

63
      # rubocop: disable Metrics/AbcSize
64
      # rubocop: disable CodeReuse/ActiveRecord
65 66 67
      def system_usage_data
        {
          counts: {
68 69 70 71 72 73 74 75 76 77 78 79 80 81
            assignee_lists: count(List.assignee),
            boards: count(Board),
            ci_builds: count(::Ci::Build),
            ci_internal_pipelines: count(::Ci::Pipeline.internal),
            ci_external_pipelines: count(::Ci::Pipeline.external),
            ci_pipeline_config_auto_devops: count(::Ci::Pipeline.auto_devops_source),
            ci_pipeline_config_repository: count(::Ci::Pipeline.repository_source),
            ci_runners: count(::Ci::Runner),
            ci_triggers: count(::Ci::Trigger),
            ci_pipeline_schedules: count(::Ci::PipelineSchedule),
            auto_devops_enabled: count(::ProjectAutoDevops.enabled),
            auto_devops_disabled: count(::ProjectAutoDevops.disabled),
            deploy_keys: count(DeployKey),
            deployments: count(Deployment),
82 83
            successful_deployments: count(Deployment.success),
            failed_deployments: count(Deployment.failed),
84 85 86
            environments: count(::Environment),
            clusters: count(::Clusters::Cluster),
            clusters_enabled: count(::Clusters::Cluster.enabled),
87 88
            project_clusters_enabled: count(::Clusters::Cluster.enabled.project_type),
            group_clusters_enabled: count(::Clusters::Cluster.enabled.group_type),
89
            instance_clusters_enabled: count(::Clusters::Cluster.enabled.instance_type),
90
            clusters_disabled: count(::Clusters::Cluster.disabled),
91 92
            project_clusters_disabled: count(::Clusters::Cluster.disabled.project_type),
            group_clusters_disabled: count(::Clusters::Cluster.disabled.group_type),
93
            instance_clusters_disabled: count(::Clusters::Cluster.disabled.instance_type),
94 95
            clusters_platforms_eks: count(::Clusters::Cluster.aws_installed.enabled),
            clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled),
96
            clusters_platforms_user: count(::Clusters::Cluster.user_provided.enabled),
97 98 99
            clusters_applications_helm: count(::Clusters::Applications::Helm.available),
            clusters_applications_ingress: count(::Clusters::Applications::Ingress.available),
            clusters_applications_cert_managers: count(::Clusters::Applications::CertManager.available),
100
            clusters_applications_crossplane: count(::Clusters::Applications::Crossplane.available),
101 102 103
            clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.available),
            clusters_applications_runner: count(::Clusters::Applications::Runner.available),
            clusters_applications_knative: count(::Clusters::Applications::Knative.available),
104
            clusters_applications_elastic_stack: count(::Clusters::Applications::ElasticStack.available),
105
            clusters_applications_jupyter: count(::Clusters::Applications::Jupyter.available),
106
            clusters_management_project: count(::Clusters::Cluster.with_management_project),
107
            in_review_folder: count(::Environment.in_review_folder),
108
            grafana_integrated_projects: count(GrafanaIntegration.enabled),
109 110
            groups: count(Group),
            issues: count(Issue),
111
            issues_created_from_gitlab_error_tracking_ui: count(SentryIssue),
112
            issues_with_associated_zoom_link: count(ZoomMeeting.added_to_issue),
113
            issues_using_zoom_quick_actions: distinct_count(ZoomMeeting, :issue_id),
114
            issues_with_embedded_grafana_charts_approx: grafana_embed_usage_data,
115
            incident_issues: count(::Issue.authored(::User.alert_bot)),
116 117 118 119 120 121
            keys: count(Key),
            label_lists: count(List.label),
            lfs_objects: count(LfsObject),
            milestone_lists: count(List.milestone),
            milestones: count(Milestone),
            pages_domains: count(PagesDomain),
122
            pool_repositories: count(PoolRepository),
123 124
            projects: count(Project),
            projects_imported_from_github: count(Project.where(import_type: 'github')),
125
            projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)),
L
Logan King 已提交
126
            projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)),
127
            projects_with_alerts_service_enabled: count(AlertsService.active),
128
            projects_with_prometheus_alerts: distinct_count(PrometheusAlert, :project_id),
129 130 131 132
            protected_branches: count(ProtectedBranch),
            releases: count(Release),
            remote_mirrors: count(RemoteMirror),
            snippets: count(Snippet),
133 134
            suggestions: count(Suggestion),
            todos: count(Todo),
135
            uploads: count(Upload),
136 137 138 139
            web_hooks: count(WebHook),
            labels: count(Label),
            merge_requests: count(MergeRequest),
            notes: count(Note)
140 141 142
          }.merge(
            services_usage,
            usage_counters,
143 144
            user_preferences_usage,
            ingress_modsecurity_usage
145 146
          )
        }
147
      end
148
      # rubocop: enable CodeReuse/ActiveRecord
149
      # rubocop: enable Metrics/AbcSize
150

151
      def cycle_analytics_usage_data
152
        Gitlab::CycleAnalytics::UsageData.new.to_json
153 154
      rescue ActiveRecord::StatementInvalid
        { avg_cycle_analytics: {} }
155 156
      end

157 158 159 160 161 162 163 164
      # rubocop:disable CodeReuse/ActiveRecord
      def grafana_embed_usage_data
        count(Issue.joins('JOIN grafana_integrations USING (project_id)')
          .where("issues.description LIKE '%' || grafana_integrations.grafana_url || '%'")
          .where(grafana_integrations: { enabled: true }))
      end
      # rubocop: enable CodeReuse/ActiveRecord

165 166 167 168 169 170
      def features_usage_data
        features_usage_data_ce
      end

      def features_usage_data_ce
        {
171
          container_registry_enabled: alt_usage_data { Gitlab.config.registry.enabled },
172
          dependency_proxy_enabled: Gitlab.config.try(:dependency_proxy)&.enabled,
173 174 175 176 177 178 179 180 181 182
          gitlab_shared_runners_enabled: alt_usage_data { Gitlab.config.gitlab_ci.shared_runners_enabled },
          gravatar_enabled: alt_usage_data { Gitlab::CurrentSettings.gravatar_enabled? },
          influxdb_metrics_enabled: alt_usage_data { Gitlab::Metrics.influx_metrics_enabled? },
          ldap_enabled: alt_usage_data { Gitlab.config.ldap.enabled },
          mattermost_enabled: alt_usage_data { Gitlab.config.mattermost.enabled },
          omniauth_enabled: alt_usage_data { Gitlab::Auth.omniauth_enabled? },
          prometheus_metrics_enabled: alt_usage_data { Gitlab::Metrics.prometheus_metrics_enabled? },
          reply_by_email_enabled: alt_usage_data { Gitlab::IncomingEmail.enabled? },
          signup_enabled: alt_usage_data { Gitlab::CurrentSettings.allow_signup? },
          web_ide_clientside_preview_enabled: alt_usage_data { Gitlab::CurrentSettings.web_ide_clientside_preview_enabled? },
183
          ingress_modsecurity_enabled: Feature.enabled?(:ingress_modsecurity)
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
        }.merge(features_usage_data_container_expiration_policies)
      end

      # rubocop: disable CodeReuse/ActiveRecord
      def features_usage_data_container_expiration_policies
        results = {}
        start = ::Project.minimum(:id)
        finish = ::Project.maximum(:id)

        results[:projects_with_expiration_policy_disabled] = distinct_count(::ContainerExpirationPolicy.where(enabled: false), :project_id, start: start, finish: finish)
        base = ::ContainerExpirationPolicy.active
        results[:projects_with_expiration_policy_enabled] = distinct_count(base, :project_id, start: start, finish: finish)

        %i[keep_n cadence older_than].each do |option|
          ::ContainerExpirationPolicy.public_send("#{option}_options").keys.each do |value| # rubocop: disable GitlabSecurity/PublicSend
            results["projects_with_expiration_policy_enabled_with_#{option}_set_to_#{value}".to_sym] = distinct_count(base.where(option => value), :project_id, start: start, finish: finish)
          end
        end

        results[:projects_with_expiration_policy_enabled_with_keep_n_unset] = distinct_count(base.where(keep_n: nil), :project_id, start: start, finish: finish)
        results[:projects_with_expiration_policy_enabled_with_older_than_unset] = distinct_count(base.where(older_than: nil), :project_id, start: start, finish: finish)

        results
207
      end
208
      # rubocop: enable CodeReuse/ActiveRecord
209

A
Alex Kalderimis 已提交
210
      # @return [Hash<Symbol, Integer>]
T
Tiago Botelho 已提交
211
      def usage_counters
212
        usage_data_counters.map { |counter| redis_usage_data(counter) }.reduce({}, :merge)
A
Alex Kalderimis 已提交
213 214 215 216
      end

      # @return [Array<#totals>] An array of objects that respond to `#totals`
      def usage_data_counters
217
        [
218 219 220 221 222 223 224 225
          Gitlab::UsageDataCounters::WikiPageCounter,
          Gitlab::UsageDataCounters::WebIdeCounter,
          Gitlab::UsageDataCounters::NoteCounter,
          Gitlab::UsageDataCounters::SnippetCounter,
          Gitlab::UsageDataCounters::SearchCounter,
          Gitlab::UsageDataCounters::CycleAnalyticsCounter,
          Gitlab::UsageDataCounters::ProductivityAnalyticsCounter,
          Gitlab::UsageDataCounters::SourceCodeCounter,
226 227
          Gitlab::UsageDataCounters::MergeRequestCounter,
          Gitlab::UsageDataCounters::DesignsCounter
228
        ]
T
Tiago Botelho 已提交
229 230
      end

231 232
      def components_usage_data
        {
233 234 235 236 237 238 239 240 241 242 243 244 245 246
          git: { version: alt_usage_data { Gitlab::Git.version } },
          gitaly: {
            version: alt_usage_data { Gitaly::Server.all.first.server_version },
            servers: alt_usage_data { Gitaly::Server.count },
            filesystems: alt_usage_data { Gitaly::Server.filesystems }
          },
          gitlab_pages: {
            enabled: alt_usage_data { Gitlab.config.pages.enabled },
            version: alt_usage_data { Gitlab::Pages::VERSION }
          },
          database: {
            adapter: alt_usage_data { Gitlab::Database.adapter_name },
            version: alt_usage_data { Gitlab::Database.version }
          },
247
          app_server: { type: app_server_type }
248
        }
249
      end
250

251 252 253 254 255 256 257 258
      def app_server_type
        Gitlab::Runtime.identify.to_s
      rescue Gitlab::Runtime::IdentificationError => e
        Gitlab::AppLogger.error(e.message)
        Gitlab::ErrorTracking.track_exception(e)
        'unknown_app_server_type'
      end

259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
      def object_store_config(component)
        config = alt_usage_data(fallback: nil) do
          Settings[component]['object_store']
        end

        if config
          {
            enabled: alt_usage_data { Settings[component]['enabled'] },
            object_store: {
              enabled: alt_usage_data { config['enabled'] },
              direct_upload: alt_usage_data { config['direct_upload'] },
              background_upload: alt_usage_data { config['background_upload'] },
              provider: alt_usage_data { config['connection']['provider'] }
            }
          }
        else
          {
            enabled: alt_usage_data { Settings[component]['enabled'] }
          }
        end
      end

      def object_store_usage_data
        {
          object_store: {
            artifacts: object_store_config('artifacts'),
            external_diffs: object_store_config('external_diffs'),
            lfs: object_store_config('lfs'),
            uploads: object_store_config('uploads'),
            packages: object_store_config('packages')
          }
        }
      end

293
      def ingress_modsecurity_usage
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
        ##
        # This method measures usage of the Modsecurity Web Application Firewall across the entire
        # instance's deployed environments.
        #
        # NOTE: this service is an approximation as it does not yet take into account if environment
        # is enabled and only measures applications installed using GitLab Managed Apps (disregards
        # CI-based managed apps).
        #
        # More details: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28331#note_318621786
        ##

        column = ::Deployment.arel_table[:environment_id]
        {
          ingress_modsecurity_logging: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_enabled.logging), column),
          ingress_modsecurity_blocking: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_enabled.blocking), column),
          ingress_modsecurity_disabled: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_disabled), column),
          ingress_modsecurity_not_installed: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_not_installed), column)
        }
312 313
      end

314
      # rubocop: disable CodeReuse/ActiveRecord
315
      def services_usage
316 317
        results = Service.available_services_names.without('jira').each_with_object({}) do |service_name, response|
          response["projects_#{service_name}_active".to_sym] = count(Service.active.where(template: false, type: "#{service_name}_service".camelize))
318
        end
319

320 321 322 323
        # Keep old Slack keys for backward compatibility, https://gitlab.com/gitlab-data/analytics/issues/3241
        results[:projects_slack_notifications_active] = results[:projects_slack_active]
        results[:projects_slack_slash_active] = results[:projects_slack_slash_commands_active]

324
        results.merge(jira_usage).merge(jira_import_usage)
325 326 327 328 329 330
      end

      def jira_usage
        # Jira Cloud does not support custom domains as per https://jira.atlassian.com/browse/CLOUD-6999
        # so we can just check for subdomains of atlassian.net

331 332 333
        results = {
          projects_jira_server_active: 0,
          projects_jira_cloud_active: 0,
334
          projects_jira_active: 0
335
        }
336

337 338
        Service.active
          .by_type(:JiraService)
339 340 341
          .includes(:jira_tracker_data)
          .find_in_batches(batch_size: BATCH_SIZE) do |services|
          counts = services.group_by do |service|
342
            # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
343 344 345 346 347 348
            service_url = service.data_fields&.url || (service.properties && service.properties['url'])
            service_url&.include?('.atlassian.net') ? :cloud : :server
          end

          results[:projects_jira_server_active] += counts[:server].count if counts[:server]
          results[:projects_jira_cloud_active] += counts[:cloud].count if counts[:cloud]
349
          results[:projects_jira_active] += services.size
350 351 352
        end

        results
353
      rescue ActiveRecord::StatementInvalid
354
        { projects_jira_server_active: FALLBACK, projects_jira_cloud_active: FALLBACK, projects_jira_active: FALLBACK }
355
      end
356 357 358 359 360 361 362

      def successful_deployments_with_cluster(scope)
        scope
          .joins(cluster: :deployments)
          .merge(Clusters::Cluster.enabled)
          .merge(Deployment.success)
      end
363
      # rubocop: enable CodeReuse/ActiveRecord
364

365 366 367 368 369 370 371 372 373 374
      def jira_import_usage
        finished_jira_imports = JiraImportState.finished

        {
          jira_imports_total_imported_count: count(finished_jira_imports),
          jira_imports_projects_count: distinct_count(finished_jira_imports, :project_id),
          jira_imports_total_imported_issues_count: alt_usage_data { JiraImportState.finished_imports_count }
        }
      end

375 376 377 378
      def user_preferences_usage
        {} # augmented in EE
      end

379
      def count(relation, column = nil, batch: true, start: nil, finish: nil)
380
        if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
381
          Gitlab::Database::BatchCount.batch_count(relation, column, start: start, finish: finish)
382 383 384 385
        else
          relation.count
        end
      rescue ActiveRecord::StatementInvalid
386
        FALLBACK
387 388
      end

389
      def distinct_count(relation, column = nil, batch: true, start: nil, finish: nil)
390
        if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
391
          Gitlab::Database::BatchCount.batch_distinct_count(relation, column, start: start, finish: finish)
392 393 394
        else
          relation.distinct_count_by(column)
        end
395
      rescue ActiveRecord::StatementInvalid
396
        FALLBACK
397
      end
398

399
      def alt_usage_data(value = nil, fallback: FALLBACK, &block)
400 401 402 403 404 405 406 407 408
        if block_given?
          yield
        else
          value
        end
      rescue
        fallback
      end

409 410 411 412 413 414 415 416
      def redis_usage_data(counter = nil, &block)
        if block_given?
          redis_usage_counter(&block)
        elsif counter.present?
          redis_usage_data_totals(counter)
        end
      end

417 418
      private

419 420 421
      def redis_usage_counter
        yield
      rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
422
        FALLBACK
423 424 425 426 427 428 429 430
      end

      def redis_usage_data_totals(counter)
        counter.totals
      rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
        counter.fallback_totals
      end

431 432 433 434 435 436 437
      def installation_type
        if Rails.env.production?
          Gitlab::INSTALLATION_TYPE
        else
          "gitlab-development-kit"
        end
      end
438 439 440
    end
  end
end
441 442

Gitlab::UsageData.prepend_if_ee('EE::Gitlab::UsageData')