Skip to content

Commit 5739d9c

Browse files
Improve xpack integration test UX (#18919) (#18934)
- Make ES and Filebeat extraction incremental. copyEs and copyFilebeat now declare inputs / outputs so Gradle skips re-extraction when the tar.gz is unchanged. - Support running a single xpack spec via `-PrubyIntegrationSpecs` to align with core. Example: `./gradlew :logstash-xpack:rubyIntegrationTests -PrubyIntegrationSpecs=qa/integration/monitoring/monitoring_is_disabled_spec.rb` (cherry picked from commit fcfcea0) # Conflicts: # x-pack/AGENTS.md Co-authored-by: Kaise <69120390+kaisecheng@users.noreply.github.com>
1 parent d17f964 commit 5739d9c

File tree

4 files changed

+135
-36
lines changed

4 files changed

+135
-36
lines changed

build.gradle

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -547,8 +547,38 @@ tasks.register("prepareFilebeatDownload") {
547547
}
548548
}
549549

550+
def verifyPackageSHA(String artifactProject, String version, String packageName) {
551+
def res = SnapshotArtifactURLs.packageUrls(artifactProject, version, packageName)
552+
String remoteSHACode = res.packageShaUrl.toURL().text.split(' ')[0]
553+
554+
def localArchive = new File("${projectDir}/build/${packageName}.tar.gz")
555+
if (localArchive.exists()) {
556+
ant.checksum(file: localArchive, algorithm: "SHA-512", forceoverwrite: true)
557+
String localSHA = new File("${projectDir}/build/${packageName}.tar.gz.SHA-512").text.trim()
558+
if (localSHA != remoteSHACode) {
559+
println "${artifactProject} package calculated fingerprint is different from remote, deleting local archive"
560+
delete(localArchive)
561+
} else {
562+
println "Local ${artifactProject} package is already the latest"
563+
}
564+
}
565+
}
566+
567+
tasks.register("checkFilebeatSHA") {
568+
dependsOn configureArtifactInfo
569+
570+
description = "Download Filebeat version remote's fingerprint file"
571+
572+
def projectRef = project
573+
doLast {
574+
String beatsVersion = projectRef.ext.get("artifactApiVersion")
575+
String downloadedFilebeatName = "filebeat-${beatsVersion}-${projectRef.ext.get("beatsArchitecture")}"
576+
verifyPackageSHA("beats", beatsVersion, downloadedFilebeatName)
577+
}
578+
}
579+
550580
tasks.register("downloadFilebeat", Download) {
551-
dependsOn prepareFilebeatDownload
581+
dependsOn prepareFilebeatDownload, checkFilebeatSHA
552582
description = "Download Filebeat Snapshot for current branch version: ${version}"
553583

554584
project.ext.set("versionFound", true)
@@ -573,16 +603,18 @@ tasks.register("deleteLocalFilebeat", Delete) {
573603
}
574604

575605
tasks.register("copyFilebeat") {
576-
dependsOn = [downloadFilebeat, deleteLocalFilebeat]
606+
dependsOn downloadFilebeat
607+
inputs.files(tasks.named("downloadFilebeat"))
608+
outputs.dir('./build/filebeat')
609+
mustRunAfter tasks.named("unpackTarDistribution")
577610
doLast {
611+
delete('./build/filebeat')
578612
copy {
579613
from tarTree(resources.gzip(project.ext.filebeatDownloadLocation))
580614
into "./build/"
581615
}
582616
file("./build/${project.ext.unpackedFilebeatName}").renameTo('./build/filebeat')
583617
System.out.println "Unzipped ${project.ext.filebeatDownloadLocation} to ./build/filebeat"
584-
System.out.println "Deleting ${project.ext.filebeatDownloadLocation}"
585-
delete(project.ext.filebeatDownloadLocation)
586618
}
587619
}
588620

@@ -595,34 +627,7 @@ tasks.register("checkEsSHA") {
595627
doLast {
596628
String esVersion = projectRef.ext.get("artifactApiVersion")
597629
String downloadedElasticsearchName = "elasticsearch-${esVersion}-${projectRef.ext.get("esArchitecture")}"
598-
String remoteSHA
599-
600-
def res = SnapshotArtifactURLs.packageUrls("elasticsearch", esVersion, downloadedElasticsearchName)
601-
remoteSHA = res.packageShaUrl.toURL().text
602-
603-
def localESArchive = new File("${projectDir}/build/${downloadedElasticsearchName}.tar.gz")
604-
if (localESArchive.exists()) {
605-
// this create a file named localESArchive with ".SHA-512" postfix
606-
ant.checksum(file: localESArchive, algorithm: "SHA-512", forceoverwrite: true)
607-
608-
File localESCalculatedSHAFile = new File("${projectDir}/build/${downloadedElasticsearchName}.tar.gz.SHA-512")
609-
String localESCalculatedSHA = localESCalculatedSHAFile.text.trim()
610-
def splitted = remoteSHA.split(' ')
611-
String remoteSHACode = splitted[0]
612-
if (localESCalculatedSHA != remoteSHACode) {
613-
println "ES package calculated fingerprint is different from remote, deleting local archive"
614-
delete(localESArchive)
615-
} else {
616-
println "Local ES package is already the latest"
617-
}
618-
}/* else {
619-
mkdir project.buildDir
620-
// touch the SHA file else downloadEs task doesn't start, this file his input for the other task
621-
new File("${projectDir}/build/${downloadedElasticsearchName}.tar.gz.SHA-512").withWriter { w ->
622-
w << "${downloadedElasticsearchName} not yet downloaded"
623-
w.close()
624-
}
625-
}*/
630+
verifyPackageSHA("elasticsearch", esVersion, downloadedElasticsearchName)
626631
}
627632
}
628633

@@ -668,17 +673,19 @@ tasks.register("deleteLocalEs", Delete) {
668673
}
669674

670675
tasks.register("copyEs") {
671-
dependsOn = [downloadEs, deleteLocalEs]
676+
dependsOn downloadEs
677+
inputs.files(tasks.named("downloadEs"))
678+
outputs.dir('./build/elasticsearch')
679+
mustRunAfter tasks.named("unpackTarDistribution")
672680
doLast {
673681
println "copyEs executing.."
682+
delete('./build/elasticsearch')
674683
copy {
675684
from tarTree(resources.gzip(project.ext.elasticsearchDownloadLocation))
676685
into "./build/"
677686
}
678-
679687
file("./build/${project.ext.unpackedElasticsearchName}").renameTo('./build/elasticsearch')
680688
println "Unzipped ${project.ext.elasticsearchDownloadLocation} to ./build/elasticsearch"
681-
println "Deleting ${project.ext.elasticsearchDownloadLocation}"
682689
}
683690
}
684691

x-pack/AGENTS.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# x-pack/AGENTS.md
2+
3+
Guidance for coding agents working with X-Pack (Elastic-licensed) features. See also the root `AGENTS.md` for general project conventions.
4+
5+
## Overview
6+
7+
X-Pack provides commercial features that extend the Logstash core. Most features integrate via the **UniversalPlugin** extension pattern; License Checking is a standalone library used by the other features. X-Pack is a separate Ruby package under `x-pack/` with its own test suite. Build without X-Pack by setting `OSS=true`.
8+
9+
## Features
10+
11+
| Feature | Path | Purpose |
12+
|---------|------|---------|
13+
| **Monitoring** | `lib/monitoring/` | Collects JVM, system, and pipeline metrics; ships to Elasticsearch |
14+
| **Config Management** | `lib/config_management/` | Fetches pipeline configs from Elasticsearch/Kibana |
15+
| **GeoIP Database Management** | `lib/geoip_database_management/` | Auto-downloads and updates GeoIP databases from Elastic CDN |
16+
| **License Checking** | `lib/license_checker/` | Validates Elasticsearch license for feature gating |
17+
18+
## Extension Pattern
19+
20+
Monitoring, Config Management, and GeoIP follow the same integration pattern. Each has an **extension class** inheriting from `LogStash::UniversalPlugin` that implements two methods:
21+
22+
1. **`additionals_settings(settings)`** — Registers `xpack.*` configuration settings with the core settings registry.
23+
2. **`register_hooks(hooks)`** — Registers lifecycle callbacks with `LogStash::Runner` (e.g. `before_bootstrap_checks`, `after_bootstrap_checks`).
24+
25+
**Entry point:** `lib/x-pack/logstash_registry.rb` registers all three extensions plus built-in input/output plugins with `LogStash::PLUGIN_REGISTRY`.
26+
27+
**Extension files:**
28+
- `lib/config_management/extension.rb`
29+
- `lib/geoip_database_management/extension.rb`
30+
- `lib/monitoring/monitoring.rb` (extension at bottom of file)
31+
32+
### Adding New Settings
33+
34+
Use `LogStash::Setting::*` classes in `additionals_settings`:
35+
36+
```ruby
37+
def additionals_settings(settings)
38+
settings.register(LogStash::Setting::BooleanSetting.new("xpack.feature.enabled", false))
39+
settings.register(LogStash::Setting::TimeValueSetting.new("xpack.feature.interval", "5s"))
40+
settings.register(LogStash::Setting::ArrayCoercible.new("xpack.feature.hosts", String, ["localhost"]))
41+
settings.register(LogStash::Setting::NullableStringSetting.new("xpack.feature.password"))
42+
end
43+
```
44+
45+
All X-Pack settings use the `xpack.` prefix. Elasticsearch connection options (hosts, SSL, auth, proxy) are shared across features via the `ElasticsearchOptions` helper mixin.
46+
47+
## License Checking
48+
49+
Features that require a commercial license use the `Licensed` mixin (`lib/license_checker/licensed.rb`):
50+
51+
1. Call `setup_license_checker(FEATURE_NAME)` during initialization.
52+
2. Wrap feature logic in `with_license_check(raise_on_error) { ... }`.
53+
3. Override `populate_license_state(xpack_info, is_serverless)` to return `{ :state => :ok | :error, :log_level => ..., :log_message => ... }`.
54+
55+
The `LicenseManager` (`lib/license_checker/license_manager.rb`) polls Elasticsearch every 30 seconds and notifies observers on state changes. License types: `trial`, `basic`, `standard`, `gold`, `platinum`, `enterprise`. Config management requires trial or above (not basic).
56+
57+
## Running Tests
58+
59+
```bash
60+
# Unit tests (from repo root)
61+
./gradlew :logstash-xpack:rubyTests
62+
63+
# Integration tests (requires running Elasticsearch)
64+
./gradlew :logstash-xpack:rubyIntegrationTests
65+
66+
# Single integration spec
67+
./gradlew :logstash-xpack:rubyIntegrationTests \
68+
-PrubyIntegrationSpecs=qa/integration/management/multiple_pipelines_spec.rb
69+
```
70+
71+
### Test Structure
72+
73+
- **Unit specs:** `x-pack/spec/` — Organized by feature (`spec/monitoring/`, `spec/config_management/`, `spec/geoip_database_management/`, `spec/license_checker/`).
74+
- **Integration tests:** `x-pack/qa/integration/` — Subdirectories for `management/`, `monitoring/`, and `fips-validation/`.
75+
- **Test helpers:** `x-pack/spec/support/helpers.rb` and `x-pack/spec/support/matchers.rb`.
76+
- **Test runners:** JUnit-based RSpec invokers in `x-pack/src/test/java/org/logstash/xpack/test/` (`RSpecTests.java` for unit, `RSpecIntegrationTests.java` for integration).
77+
78+
### GeoIP Test Data
79+
80+
The `unzipGeolite` Gradle task downloads GeoLite2 test databases. Unit tests for GeoIP depend on this task running first (handled automatically by the `rubyTests` dependency chain).
81+
82+
## Key Architectural Patterns
83+
84+
- **Hook-based integration.** Config management intercepts bootstrap checks to swap the local config source with an Elasticsearch source. Monitoring adds an internal pipeline after the agent starts. Neither modifies core code directly.
85+
- **Singleton managers.** `GeoipDatabaseManagement::Manager` and `LicenseChecker::LicenseManager` are singletons with thread-safe initialization via Mutex.
86+
- **Observer pattern.** Database subscriptions and license managers notify observers on state changes, enabling features to react dynamically without polling.
87+
- **Internal pipelines.** Monitoring generates its pipeline config from an ERB template (`lib/template.cfg.erb`) and injects it via `InternalPipelineSource`. These run alongside user pipelines but are hidden from the user-facing API.

x-pack/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ tasks.register("rubyIntegrationTests", Test) {
7070
inputs.files fileTree("${rootProject.projectDir}/Gemfile.lock")
7171
inputs.files fileTree("${rootProject.projectDir}/logstash-core/lib")
7272
systemProperty 'logstash.root.dir', projectDir.parent
73+
if (project.hasProperty('rubyIntegrationSpecs')) {
74+
systemProperty 'org.logstash.xpack.integration.specs', project.property('rubyIntegrationSpecs')
75+
}
76+
outputs.upToDateWhen { false }
7377
include '/org/logstash/xpack/test/RSpecIntegrationTests.class'
7478
}
7579

x-pack/src/test/java/org/logstash/xpack/test/RSpecIntegrationTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ public class RSpecIntegrationTests extends RSpecTests {
1616

1717
@Override
1818
protected List<String> rspecArgs() {
19-
return Arrays.asList("-fd", "qa/integration");
19+
String specs = System.getProperty("org.logstash.xpack.integration.specs", "qa/integration");
20+
return Arrays.asList("-fd", specs);
2021
}
2122

2223
@Test

0 commit comments

Comments
 (0)