• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Helvethink / gitlab-ci-exporter / 18286274506

06 Oct 2025 03:36PM UTC coverage: 11.405% (+1.6%) from 9.811%
18286274506

push

github

web-flow
Release - RC1 (#74)

* Improve unit tests

* Implementer beter management for redis

* Fix test unit

* Bump docker/login-action from 3.4.0 to 3.5.0 (#80)

Bumps [docker/login-action](https://github.com/docker/login-action) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/74a5d1423...184bdaa07)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump step-security/harden-runner from 2.12.1 to 2.13.0 (#78)

Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.12.1 to 2.13.0.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/v2.12.1...ec9f2d574)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump github/codeql-action from 3.29.0 to 3.29.9 (#77)

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.0 to 3.29.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/ce28f5bb4...df559355d)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-ve... (continued)

20 of 208 new or added lines in 9 files covered. (9.62%)

3 existing lines in 2 files now uncovered.

818 of 7172 relevant lines covered (11.41%)

0.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/pkg/controller/controller.go
1
package controller
2

3
import (
4
        "context"
5

6
        "github.com/google/uuid"
7
        "github.com/pkg/errors"
8
        "github.com/redis/go-redis/extra/redisotel/v9"
9
        "github.com/redis/go-redis/v9"
10
        log "github.com/sirupsen/logrus"
11
        "github.com/vmihailenco/taskq/v4"
12
        "go.opentelemetry.io/otel"
13
        "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
14
        "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
15
        "go.opentelemetry.io/otel/sdk/resource"
16
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
17
        semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
18
        "google.golang.org/grpc"
19

20
        "github.com/helvethink/gitlab-ci-exporter/pkg/config"
21
        "github.com/helvethink/gitlab-ci-exporter/pkg/gitlab"
22
        "github.com/helvethink/gitlab-ci-exporter/pkg/ratelimit"
23
        "github.com/helvethink/gitlab-ci-exporter/pkg/schemas"
24
        "github.com/helvethink/gitlab-ci-exporter/pkg/store"
25
)
26

27
const tracerName = "gitlab-ci-pipelines-exporter"
28

29
// Controller holds the necessary clients and components to run the application and handle its operations.
30
// It includes configuration, connections to Redis, GitLab client, storage interface, and task management.
31
// The UUID field uniquely identifies this controller instance, especially useful in clustered deployments
32
// where multiple exporter instances share Redis.
33
type Controller struct {
34
        Config         config.Config  // Application configuration settings
35
        Redis          *redis.Client  // Redis client for caching and coordination
36
        Gitlab         *gitlab.Client // GitLab API client
37
        Store          store.Store    // Storage interface to persist data (backed by Redis)
38
        TaskController TaskController // Manages background tasks and job queues
39

40
        // UUID uniquely identifies this controller instance among others when running
41
        // in clustered mode, facilitating coordination via Redis.
42
        UUID uuid.UUID
43
}
44

45
// New creates and initializes a new Controller instance.
46
// It sets up tracing, Redis connection, task controller, storage, GitLab client, and starts the scheduler.
47
//
48
// Parameters:
49
// - ctx: Context for cancellation and deadlines.
50
// - cfg: Configuration object with all needed parameters.
51
// - version: Version string of the running application (used in GitLab client configuration).
52
//
53
// Returns:
54
// - c: Initialized Controller instance.
55
// - err: Any error encountered during setup.
56
func New(ctx context.Context, cfg config.Config, version string) (c Controller, err error) {
×
57
        c.Config = cfg      // Store configuration
×
58
        c.UUID = uuid.New() // Generate a new UUID for this controller instance
×
59

×
60
        // Configure distributed tracing if an OpenTelemetry gRPC endpoint is specified
×
61
        if err = configureTracing(ctx, cfg.OpenTelemetry.GRPCEndpoint); err != nil {
×
62
                return
×
63
        }
×
64

65
        // Initialize Redis connection with provided URL
NEW
66
        if err = c.configureRedis(ctx, &cfg.Redis); err != nil {
×
67
                return
×
68
        }
×
69

70
        // Create a task controller to manage job queues with a maximum size from config
71
        c.TaskController = NewTaskController(ctx, c.Redis, cfg.Gitlab.MaximumJobsQueueSize)
×
72
        c.registerTasks() // Register the tasks that the controller can run
×
73

×
74
        // Initialize the storage interface which will use Redis and configured projects
×
NEW
75
        var redisStore *store.Redis
×
NEW
76
        if c.Redis != nil {
×
NEW
77
                redisStore = store.NewRedisStore(c.Redis, store.WithTTLConfig(&store.RedisTTLConfig{
×
NEW
78
                        Project:     cfg.Redis.ProjectTTL,
×
NEW
79
                        Environment: cfg.Redis.EnvTTL,
×
NEW
80
                        Refs:        cfg.Redis.RefTTL,
×
NEW
81
                        Runner:      cfg.Redis.RunnerTTL,
×
NEW
82
                        Metrics:     cfg.Redis.MetricTTL,
×
NEW
83
                }))
×
NEW
84
        }
×
NEW
85
        c.Store = store.New(ctx, redisStore, c.Config.Projects)
×
86

×
87
        // Configure GitLab client, passing the app version for client identification
×
88
        if err = c.configureGitlab(cfg.Gitlab, version); err != nil {
×
89
                return
×
90
        }
×
91

92
        // Start background schedulers for pulling data and garbage collection based on config
93
        c.Schedule(ctx, cfg.Pull, cfg.GarbageCollect)
×
94

×
95
        return
×
96
}
97

98
// registerTasks registers all task handlers with the TaskController's task map.
99
// It iterates over a map where each key is a task type and each value is the corresponding handler method.
100
// For each task type, it registers the handler with a retry limit of 1, meaning the task will be retried once on failure.
101
// This setup enables the Controller to handle various asynchronous tasks related to garbage collection, pulling metrics, environments, projects, and refs.
102
func (c *Controller) registerTasks() {
×
103
        for n, h := range map[schemas.TaskType]interface{}{
×
104
                schemas.TaskTypeGarbageCollectEnvironments:   c.TaskHandlerGarbageCollectEnvironments,
×
105
                schemas.TaskTypeGarbageCollectMetrics:        c.TaskHandlerGarbageCollectMetrics,
×
106
                schemas.TaskTypeGarbageCollectProjects:       c.TaskHandlerGarbageCollectProjects,
×
107
                schemas.TaskTypeGarbageCollectRefs:           c.TaskHandlerGarbageCollectRefs,
×
108
                schemas.TaskTypeGarbageCollectRunners:        c.TaskHandlerGarbageCollectRunners,
×
109
                schemas.TaskTypePullEnvironmentMetrics:       c.TaskHandlerPullEnvironmentMetrics,
×
110
                schemas.TaskTypePullEnvironmentsFromProject:  c.TaskHandlerPullEnvironmentsFromProject,
×
111
                schemas.TaskTypePullEnvironmentsFromProjects: c.TaskHandlerPullEnvironmentsFromProjects,
×
112
                schemas.TaskTypePullMetrics:                  c.TaskHandlerPullMetrics,
×
113
                schemas.TaskTypePullProject:                  c.TaskHandlerPullProject,
×
114
                schemas.TaskTypePullProjectsFromWildcard:     c.TaskHandlerPullProjectsFromWildcard,
×
115
                schemas.TaskTypePullProjectsFromWildcards:    c.TaskHandlerPullProjectsFromWildcards,
×
116
                schemas.TaskTypePullRefMetrics:               c.TaskHandlerPullRefMetrics,
×
117
                schemas.TaskTypePullRefsFromProject:          c.TaskHandlerPullRefsFromProject,
×
118
                schemas.TaskTypePullRefsFromProjects:         c.TaskHandlerPullRefsFromProjects,
×
119
                schemas.TaskTypePullRunnersMetrics:           c.TaskHandlerPullRunnerMetrics,
×
120
                schemas.TaskTypePullRunnersFromProject:       c.TaskHandlerPullRunnersFromProject,
×
121
                schemas.TaskTypePullRunnersFromProjects:      c.TaskHandlerPullRunnersFromProjects,
×
122
        } {
×
123
                _, _ = c.TaskController.TaskMap.Register(string(n), &taskq.TaskConfig{
×
124
                        Handler:    h,
×
125
                        RetryLimit: 1,
×
126
                })
×
127
        }
×
128
}
129

130
// dequeueTask attempts to remove a task identified by its type and unique ID from the task queue in the store.
131
// If the operation fails, it logs a warning with the task details and the error encountered.
132
// This helps ensure that tasks are properly cleaned up from the queue to avoid duplicate processing or stale tasks.
NEW
133
func (c *Controller) dequeueTask(ctx context.Context, tt schemas.TaskType, uniqueID string) {
×
NEW
134
        if err := c.Store.DequeueTask(ctx, tt, uniqueID); err != nil {
×
135
                log.WithContext(ctx).
×
136
                        WithFields(log.Fields{
×
137
                                "task_type":      tt,
×
138
                                "task_unique_id": uniqueID,
×
139
                        }).
×
140
                        WithError(err).
×
NEW
141
                        Warn("dequeue task")
×
142
        }
×
143
}
144

145
// configureTracing sets up OpenTelemetry tracing via a gRPC endpoint.
146
// If no endpoint is provided, tracing support is skipped.
147
func configureTracing(ctx context.Context, grpcEndpoint string) error {
×
148
        // If no gRPC endpoint is specified, log that tracing will be skipped and return nil
×
149
        if len(grpcEndpoint) == 0 {
×
150
                log.Debug("open-telemetry.grpc_endpoint is not configured, skipping open telemetry support")
×
151
                return nil
×
152
        }
×
153

154
        // Log that a gRPC endpoint is configured and tracing initialization is starting
155
        log.WithFields(log.Fields{
×
156
                "open-telemetry_grpc_endpoint": grpcEndpoint,
×
157
        }).Info("open-telemetry gRPC endpoint provided, initializing connection..")
×
158

×
159
        // Create a new OpenTelemetry gRPC trace client with insecure connection, connecting to the given endpoint,
×
160
        // and block until the connection is established
×
161
        traceClient := otlptracegrpc.NewClient(
×
162
                otlptracegrpc.WithInsecure(),
×
163
                otlptracegrpc.WithEndpoint(grpcEndpoint),
×
164
                otlptracegrpc.WithDialOption(grpc.WithBlock()), // nolint: staticcheck
×
165
        )
×
166

×
167
        // Create a new trace exporter using the gRPC trace client
×
168
        traceExp, err := otlptrace.New(ctx, traceClient)
×
169
        if err != nil {
×
170
                // Return error if exporter creation fails
×
171
                return err
×
172
        }
×
173

174
        // Create a resource describing this application with metadata from environment,
175
        // process info, telemetry SDK, host info, and set the service name attribute
176
        res, err := resource.New(ctx,
×
177
                resource.WithFromEnv(),
×
178
                resource.WithProcess(),
×
179
                resource.WithTelemetrySDK(),
×
180
                resource.WithHost(),
×
181
                resource.WithAttributes(
×
182
                        semconv.ServiceNameKey.String("gitlab-ci-exporter"),
×
183
                ),
×
184
        )
×
185
        if err != nil {
×
186
                // Return error if resource creation fails
×
187
                return err
×
188
        }
×
189

190
        // Create a batch span processor to buffer and send spans efficiently to the exporter
191
        bsp := sdktrace.NewBatchSpanProcessor(traceExp)
×
192

×
193
        // Create a tracer provider configured to always sample all traces,
×
194
        // associate the resource metadata, and use the batch span processor
×
195
        tracerProvider := sdktrace.NewTracerProvider(
×
196
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
×
197
                sdktrace.WithResource(res),
×
198
                sdktrace.WithSpanProcessor(bsp),
×
199
        )
×
200

×
201
        // Set the global tracer provider so it will be used by the OpenTelemetry API
×
202
        otel.SetTracerProvider(tracerProvider)
×
203

×
204
        // Return nil to indicate successful setup
×
205
        return nil
×
206
}
207

208
// configureGitlab initializes the GitLab client with the given configuration and version.
209
// It sets up a rate limiter using Redis if available, otherwise uses a local rate limiter.
210
func (c *Controller) configureGitlab(cfg config.Gitlab, version string) (err error) {
×
211
        var rl ratelimit.Limiter
×
212

×
213
        // If Redis client is available, create a Redis-based rate limiter
×
214
        if c.Redis != nil {
×
215
                rl = ratelimit.NewRedisLimiter(c.Redis, cfg.MaximumRequestsPerSecond)
×
216
        } else {
×
217
                // Otherwise, create a local in-memory rate limiter with configured limits
×
218
                rl = ratelimit.NewLocalLimiter(cfg.MaximumRequestsPerSecond, cfg.BurstableRequestsPerSecond)
×
219
        }
×
220

221
        // Create a new GitLab client with the provided configuration parameters:
222
        // - URL to the GitLab API
223
        // - Personal Access Token or OAuth token for authentication
224
        // - Option to disable TLS verification if specified
225
        // - User-Agent header version string to identify the client
226
        // - The configured rate limiter (Redis or local)
227
        // - Optional readiness/health check URL
228
        c.Gitlab, err = gitlab.NewClient(gitlab.ClientConfig{
×
229
                URL:              cfg.URL,
×
230
                Token:            cfg.Token,
×
231
                DisableTLSVerify: !cfg.EnableTLSVerify,
×
232
                UserAgentVersion: version,
×
233
                RateLimiter:      rl,
×
234
                ReadinessURL:     cfg.HealthURL,
×
235
        })
×
236

×
237
        // Return any error from GitLab client creation
×
238
        return
×
239
}
240

241
// configureRedis initializes the Redis client using the provided URL and sets up OpenTelemetry tracing instrumentation.
242
// It returns an error if any step of the configuration or connection fails.
NEW
243
func (c *Controller) configureRedis(ctx context.Context, config *config.Redis) (err error) {
×
244
        // Start a new OpenTelemetry trace span for monitoring this function
×
245
        ctx, span := otel.Tracer(tracerName).Start(ctx, "controller:configureRedis")
×
246
        defer span.End()
×
247

×
248
        // If no Redis URL is provided, skip Redis configuration and use local (in-memory) alternatives
×
NEW
249
        if len(config.URL) <= 0 {
×
250
                log.Debug("redis url is not configured, skipping configuration & using local driver")
×
251
                return
×
252
        }
×
253

254
        log.Info("redis url configured, initializing connection..")
×
255

×
256
        var opt *redis.Options
×
257

×
258
        // Parse the Redis URL into options; return early on error
×
NEW
259
        if opt, err = redis.ParseURL(config.URL); err != nil {
×
260
                return
×
261
        }
×
262

263
        // Create a new Redis client instance with the parsed options
264
        c.Redis = redis.NewClient(opt)
×
265

×
266
        // Instrument the Redis client with OpenTelemetry tracing for monitoring Redis operations
×
267
        if err = redisotel.InstrumentTracing(c.Redis); err != nil {
×
268
                return
×
269
        }
×
270

271
        // Test the Redis connection by sending a PING command; wrap any error with context
272
        if _, err := c.Redis.Ping(ctx).Result(); err != nil {
×
273
                return errors.Wrap(err, "connecting to redis")
×
274
        }
×
275

276
        log.Info("connected to redis")
×
277

×
278
        // Return nil error on successful initialization
×
279
        return
×
280
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc