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

3 4 5 6 7 8 9
# For hardening usage ping and make it easier to add measures there is in place alt_usage_data method
# which handles StandardError and fallbacks into -1
# this way not all measures fail if we encounter one exception
#
# Examples:
#  alt_usage_data { Gitlab::VERSION }
#  alt_usage_data { Gitlab::CurrentSettings.uuid }
10 11
module Gitlab
  class UsageData
12
    BATCH_SIZE = 100
13

14
    class << self
15
      def data(force_refresh: false)
A
Alex Kalderimis 已提交
16 17 18
        Rails.cache.fetch('usage_data', force: force_refresh, expires_in: 2.weeks) do
          uncached_data
        end
19 20 21
      end

      def uncached_data
22 23
        license_usage_data
          .merge(system_usage_data)
24 25 26
          .merge(features_usage_data)
          .merge(components_usage_data)
          .merge(cycle_analytics_usage_data)
27
          .merge(object_store_usage_data)
28 29
      end

30 31
      def to_json(force_refresh: false)
        data(force_refresh: force_refresh).to_json
32 33
      end

34
      def license_usage_data
35 36 37 38 39
        {
          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 },
40
          active_user_count: count(User.active),
41 42 43 44 45
          recorded_at: Time.now,
          edition: 'CE'
        }
      end

46
      # rubocop: disable Metrics/AbcSize
47
      # rubocop: disable CodeReuse/ActiveRecord
48 49 50
      def system_usage_data
        {
          counts: {
51 52 53 54 55 56 57 58 59 60 61 62 63 64
            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),
65 66
            successful_deployments: count(Deployment.success),
            failed_deployments: count(Deployment.failed),
67 68 69
            environments: count(::Environment),
            clusters: count(::Clusters::Cluster),
            clusters_enabled: count(::Clusters::Cluster.enabled),
70 71
            project_clusters_enabled: count(::Clusters::Cluster.enabled.project_type),
            group_clusters_enabled: count(::Clusters::Cluster.enabled.group_type),
72
            instance_clusters_enabled: count(::Clusters::Cluster.enabled.instance_type),
73
            clusters_disabled: count(::Clusters::Cluster.disabled),
74 75
            project_clusters_disabled: count(::Clusters::Cluster.disabled.project_type),
            group_clusters_disabled: count(::Clusters::Cluster.disabled.group_type),
76
            instance_clusters_disabled: count(::Clusters::Cluster.disabled.instance_type),
77 78
            clusters_platforms_eks: count(::Clusters::Cluster.aws_installed.enabled),
            clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled),
79
            clusters_platforms_user: count(::Clusters::Cluster.user_provided.enabled),
80 81 82
            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),
83
            clusters_applications_crossplane: count(::Clusters::Applications::Crossplane.available),
84 85 86
            clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.available),
            clusters_applications_runner: count(::Clusters::Applications::Runner.available),
            clusters_applications_knative: count(::Clusters::Applications::Knative.available),
87
            clusters_applications_elastic_stack: count(::Clusters::Applications::ElasticStack.available),
88
            clusters_applications_jupyter: count(::Clusters::Applications::Jupyter.available),
89
            clusters_management_project: count(::Clusters::Cluster.with_management_project),
90
            in_review_folder: count(::Environment.in_review_folder),
91
            grafana_integrated_projects: count(GrafanaIntegration.enabled),
92 93
            groups: count(Group),
            issues: count(Issue),
94
            issues_created_from_gitlab_error_tracking_ui: count(SentryIssue),
95
            issues_with_associated_zoom_link: count(ZoomMeeting.added_to_issue),
96
            issues_using_zoom_quick_actions: distinct_count(ZoomMeeting, :issue_id),
97
            issues_with_embedded_grafana_charts_approx: grafana_embed_usage_data,
98
            incident_issues: count(::Issue.authored(::User.alert_bot)),
99 100 101 102 103 104
            keys: count(Key),
            label_lists: count(List.label),
            lfs_objects: count(LfsObject),
            milestone_lists: count(List.milestone),
            milestones: count(Milestone),
            pages_domains: count(PagesDomain),
105
            pool_repositories: count(PoolRepository),
106 107
            projects: count(Project),
            projects_imported_from_github: count(Project.where(import_type: 'github')),
108
            projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)),
L
Logan King 已提交
109
            projects_with_error_tracking_enabled: count(::ErrorTracking::ProjectErrorTrackingSetting.where(enabled: true)),
110
            projects_with_alerts_service_enabled: count(AlertsService.active),
111
            projects_with_prometheus_alerts: distinct_count(PrometheusAlert, :project_id),
112 113 114 115
            protected_branches: count(ProtectedBranch),
            releases: count(Release),
            remote_mirrors: count(RemoteMirror),
            snippets: count(Snippet),
116 117
            suggestions: count(Suggestion),
            todos: count(Todo),
118
            uploads: count(Upload),
119 120 121 122
            web_hooks: count(WebHook),
            labels: count(Label),
            merge_requests: count(MergeRequest),
            notes: count(Note)
123 124 125
          }.merge(
            services_usage,
            usage_counters,
126 127
            user_preferences_usage,
            ingress_modsecurity_usage
128 129
          )
        }
130
      end
131
      # rubocop: enable CodeReuse/ActiveRecord
132
      # rubocop: enable Metrics/AbcSize
133

134
      def cycle_analytics_usage_data
135
        Gitlab::CycleAnalytics::UsageData.new.to_json
136 137
      rescue ActiveRecord::StatementInvalid
        { avg_cycle_analytics: {} }
138 139
      end

140 141 142 143 144 145 146 147
      # 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

148 149 150 151 152 153
      def features_usage_data
        features_usage_data_ce
      end

      def features_usage_data_ce
        {
154
          container_registry_enabled: alt_usage_data { Gitlab.config.registry.enabled },
155
          dependency_proxy_enabled: Gitlab.config.try(:dependency_proxy)&.enabled,
156 157 158 159 160 161 162 163 164 165
          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? },
166
          ingress_modsecurity_enabled: Feature.enabled?(:ingress_modsecurity)
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
        }.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
190
      end
191
      # rubocop: enable CodeReuse/ActiveRecord
192

A
Alex Kalderimis 已提交
193
      # @return [Hash<Symbol, Integer>]
T
Tiago Botelho 已提交
194
      def usage_counters
A
Alex Kalderimis 已提交
195 196 197 198 199
        usage_data_counters.map(&:totals).reduce({}) { |a, b| a.merge(b) }
      end

      # @return [Array<#totals>] An array of objects that respond to `#totals`
      def usage_data_counters
200
        [
201 202 203 204 205 206 207 208 209
          Gitlab::UsageDataCounters::WikiPageCounter,
          Gitlab::UsageDataCounters::WebIdeCounter,
          Gitlab::UsageDataCounters::NoteCounter,
          Gitlab::UsageDataCounters::SnippetCounter,
          Gitlab::UsageDataCounters::SearchCounter,
          Gitlab::UsageDataCounters::CycleAnalyticsCounter,
          Gitlab::UsageDataCounters::ProductivityAnalyticsCounter,
          Gitlab::UsageDataCounters::SourceCodeCounter,
          Gitlab::UsageDataCounters::MergeRequestCounter
210
        ]
T
Tiago Botelho 已提交
211 212
      end

213 214
      def components_usage_data
        {
215 216 217 218 219 220 221 222 223 224 225 226 227 228
          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 }
          },
229
          app_server: { type: app_server_type }
230
        }
231
      end
232

233 234 235 236 237 238 239 240
      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

241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
      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

275 276 277 278
      def ingress_modsecurity_usage
        ::Clusters::Applications::IngressModsecurityUsageService.new.execute
      end

279
      # rubocop: disable CodeReuse/ActiveRecord
280
      def services_usage
281 282
        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))
283
        end
284

285 286 287 288
        # 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]

289
        results.merge(jira_usage).merge(jira_import_usage)
290 291 292 293 294 295
      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

296 297 298
        results = {
          projects_jira_server_active: 0,
          projects_jira_cloud_active: 0,
299
          projects_jira_active: 0
300
        }
301

302 303
        Service.active
          .by_type(:JiraService)
304 305 306
          .includes(:jira_tracker_data)
          .find_in_batches(batch_size: BATCH_SIZE) do |services|
          counts = services.group_by do |service|
307
            # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
308 309 310 311 312 313
            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]
314
          results[:projects_jira_active] += services.size
315 316 317
        end

        results
318 319
      rescue ActiveRecord::StatementInvalid
        { projects_jira_server_active: -1, projects_jira_cloud_active: -1, projects_jira_active: -1 }
320
      end
321
      # rubocop: enable CodeReuse/ActiveRecord
322

323 324 325 326 327 328 329 330 331 332
      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

333 334 335 336
      def user_preferences_usage
        {} # augmented in EE
      end

337
      def count(relation, column = nil, fallback: -1, batch: true, start: nil, finish: nil)
338
        if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
339
          Gitlab::Database::BatchCount.batch_count(relation, column, start: start, finish: finish)
340 341 342 343 344 345 346
        else
          relation.count
        end
      rescue ActiveRecord::StatementInvalid
        fallback
      end

347
      def distinct_count(relation, column = nil, fallback: -1, batch: true, start: nil, finish: nil)
348
        if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
349
          Gitlab::Database::BatchCount.batch_distinct_count(relation, column, start: start, finish: finish)
350 351 352
        else
          relation.distinct_count_by(column)
        end
353 354 355
      rescue ActiveRecord::StatementInvalid
        fallback
      end
356

357 358 359 360 361 362 363 364 365 366 367 368
      def alt_usage_data(value = nil, fallback: -1, &block)
        if block_given?
          yield
        else
          value
        end
      rescue
        fallback
      end

      private

369 370 371 372 373 374 375
      def installation_type
        if Rails.env.production?
          Gitlab::INSTALLATION_TYPE
        else
          "gitlab-development-kit"
        end
      end
376 377 378
    end
  end
end
379 380

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