govuk_content_item_loader 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +84 -0
- data/govuk_content_item_loader.gemspec +3 -0
- data/lib/govuk_content_item_loader/govuk_conditional_content_item_loader.rb +74 -0
- data/lib/govuk_content_item_loader/govuk_graphql_traffic_rates.rb +15 -0
- data/lib/govuk_content_item_loader/version.rb +1 -1
- data/lib/govuk_content_item_loader.rb +2 -0
- metadata +45 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a533cf2cd72d38cfd19f7ef5555bb14c93156638ff6220d1dd494d92550a390
|
|
4
|
+
data.tar.gz: a1e9a897b7b82e747d81434c1d5965d550507edaa3a2a5ae4abde20df63e62ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85e6a82e33eb3fab62a93bfda0eeff17314f6ac79b4a6e9f2f09b138cbfa5c9c3812557b0638a5d294d9f28c790ee5f6b2e24293b28bd7042699e17c13b037b3
|
|
7
|
+
data.tar.gz: 7fd62a7aecc2bd6a214a92c6576a0469b3766d548ba699326f9798f7330d2f7b8fa5a219fca55fdac83e064aff2bd5a0e3ffe664ac1518ac6c8b36fe22af35f1
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -2,6 +2,90 @@
|
|
|
2
2
|
|
|
3
3
|
Ruby gem that standardises how GOV.UK frontend apps load content items. It provides configurable GraphQL traffic rates per content schema and a request-level loader that routes requests to the Publishing API via GraphQL or falls back to the Content Store ensuring consistent traffic management across applications.
|
|
4
4
|
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the gem
|
|
8
|
+
|
|
9
|
+
`gem install govuk_content_item_loader`
|
|
10
|
+
|
|
11
|
+
or add it to your Gemfile
|
|
12
|
+
|
|
13
|
+
`gem "govuk_content_item_loader"`
|
|
14
|
+
|
|
15
|
+
## GraphQL traffic rates
|
|
16
|
+
|
|
17
|
+
Provides a canonical way for frontend apps to configure GraphQL traffic rates for relevant content schemas. It sets the following application configuration:
|
|
18
|
+
|
|
19
|
+
* `Rails.application.config.graphql_allowed_schemas` - array of schema names that are eligible to be served via GraphQL.
|
|
20
|
+
* `Rails.application.config.graphql_traffic_rates` - hash mapping schema names to traffic percentages (as floats between 0 and 1.0).
|
|
21
|
+
|
|
22
|
+
Traffic rates for individual schemas are set in `govuk-helm-charts` repository as environment variables, with the following format `GRAPHQL_RATE_<SCHEMA_NAME>`.
|
|
23
|
+
|
|
24
|
+
### Usage
|
|
25
|
+
|
|
26
|
+
To enable this functionality, create a file `config/initializers/govuk_graphql_traffic_rates.rb` in the app containing:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
GovukGraphqlTrafficRates.configure
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Conditional Content Item Loader
|
|
33
|
+
|
|
34
|
+
This class acts as a traffic-splitting content loader that decides, per request, whether to fetch a content item from the **Publishing API via GraphQL** or fall back to the **Content Store**, based on request parameters and configured traffic-shaping rules based on content schemas.
|
|
35
|
+
|
|
36
|
+
**In practice, it:**
|
|
37
|
+
|
|
38
|
+
* Chooses between GraphQL (Publishing API) and the Content Store when loading a content item
|
|
39
|
+
* Always disables GraphQL on draft hosts
|
|
40
|
+
* Allows GraphQL to be explicitly forced on or off via the `graphql` request parameter
|
|
41
|
+
* Falls back to the Content Store unless the content item’s schema is explicitly allow-listed
|
|
42
|
+
* Uses a per-schema traffic rate to probabilistically route a percentage of requests to GraphQL
|
|
43
|
+
* Records GraphQL success, error status codes and timeouts in Prometheus request labels
|
|
44
|
+
* Propagates GdsApi errors
|
|
45
|
+
|
|
46
|
+
### Usage
|
|
47
|
+
|
|
48
|
+
#### Prerequisites
|
|
49
|
+
|
|
50
|
+
1. Ensure application configuration sets list of schema names that are eligible to be served via GraphQL and hash mapping schema names to traffic percentages via `GovukGraphqlTrafficRates` described above.
|
|
51
|
+
|
|
52
|
+
2. Ensure the `PLEK_SERVICE_PUBLISHING_API_URI` environment variable is set in **govuk-helm-charts** for the live app (draft stack is handled via [`PLEK_HOSTNAME_PREFIX`](https://github.com/alphagov/plek/blob/main/CHANGELOG.md#410)), across all relevant environments, so the Publishing API (GraphQL) can be correctly resolved at runtime:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
- name: PLEK_SERVICE_PUBLISHING_API_URI
|
|
56
|
+
value: "http://publishing-api-read-replica"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
3. (Assumed true for all frontend apps) Ensure the application has Content Store connection configuration.
|
|
60
|
+
|
|
61
|
+
#### Using the loader
|
|
62
|
+
|
|
63
|
+
`GovukConditionalContentItemLoader` is intended to be used at the point where a request needs to load a content item, and where traffic may be conditionally routed to GraphQL instead of the Content Store.
|
|
64
|
+
|
|
65
|
+
At its simplest, you initialise the loader with the current request and call load:
|
|
66
|
+
```
|
|
67
|
+
loader = GovukConditionalContentItemLoader.new(request: request)
|
|
68
|
+
content_item = loader.load
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
By default, the loader uses:
|
|
72
|
+
* `GdsApi.content_store` to fetch content from the Content Store
|
|
73
|
+
* `GdsApi.publishing_api` to fetch content via GraphQL
|
|
74
|
+
|
|
75
|
+
These defaults make it suitable for use in production code without additional configuration.
|
|
76
|
+
|
|
77
|
+
*Passing custom clients*
|
|
78
|
+
For testing or specialised use cases, you can explicitly pass in the API clients:
|
|
79
|
+
```
|
|
80
|
+
loader = GovukGraphql::ConditionalContentItemLoader.new(
|
|
81
|
+
request: request,
|
|
82
|
+
content_store_client: content_store_client,
|
|
83
|
+
publishing_api_client: publishing_api_client,
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The decision logic is also exposed via `can_load_from_graphql?`, allowing applications to make the routing decision themselves and implement custom fallback or error-handling behaviour if needed.
|
|
88
|
+
|
|
5
89
|
## Licence
|
|
6
90
|
|
|
7
91
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -20,7 +20,10 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
spec.require_paths = %w[lib]
|
|
21
21
|
|
|
22
22
|
spec.add_dependency "gds-api-adapters", ">= 99.3"
|
|
23
|
+
spec.add_dependency "railties", ">= 7.2"
|
|
23
24
|
|
|
25
|
+
spec.add_development_dependency "climate_control"
|
|
26
|
+
spec.add_development_dependency "ostruct"
|
|
24
27
|
spec.add_development_dependency "rails", ">= 7.2"
|
|
25
28
|
spec.add_development_dependency "rake", ">= 10.0"
|
|
26
29
|
spec.add_development_dependency "rspec", "~> 3.0"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
class GovukConditionalContentItemLoader
|
|
2
|
+
attr_reader :content_store_client, :publishing_api_client, :request, :base_path
|
|
3
|
+
|
|
4
|
+
def initialize(request:, content_store_client: GdsApi.content_store, publishing_api_client: GdsApi.publishing_api)
|
|
5
|
+
@content_store_client = content_store_client
|
|
6
|
+
@publishing_api_client = publishing_api_client
|
|
7
|
+
@request = request
|
|
8
|
+
@base_path = request&.path
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def load
|
|
12
|
+
can_load_from_graphql? ? content_item_from_graphql : content_item_from_content_store
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def can_load_from_graphql?
|
|
16
|
+
return false unless request
|
|
17
|
+
|
|
18
|
+
return false if draft_host?
|
|
19
|
+
|
|
20
|
+
return force_graphql_param unless force_graphql_param.nil?
|
|
21
|
+
|
|
22
|
+
schema_name = content_item_from_content_store["schema_name"]
|
|
23
|
+
return false unless graphql_schema_allowed?(schema_name)
|
|
24
|
+
|
|
25
|
+
within_graphql_traffic_rate?(schema_name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def content_item_from_graphql
|
|
31
|
+
set_prometheus_labels
|
|
32
|
+
publishing_api_client.graphql_live_content_item(base_path)
|
|
33
|
+
rescue GdsApi::HTTPErrorResponse => e
|
|
34
|
+
set_prometheus_labels(graphql_status_code: e.code)
|
|
35
|
+
raise e
|
|
36
|
+
rescue GdsApi::TimedOutException => e
|
|
37
|
+
set_prometheus_labels(graphql_api_timeout: true)
|
|
38
|
+
raise e
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def content_item_from_content_store
|
|
42
|
+
@content_item_from_content_store ||= content_store_client.content_item(base_path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def set_prometheus_labels(graphql_status_code: 200, graphql_api_timeout: false)
|
|
46
|
+
prometheus_labels = request.env.fetch("govuk.prometheus_labels", {})
|
|
47
|
+
|
|
48
|
+
hash = {
|
|
49
|
+
"graphql_status_code" => graphql_status_code,
|
|
50
|
+
"graphql_api_timeout" => graphql_api_timeout,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
request.env["govuk.prometheus_labels"] = prometheus_labels.merge(hash)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def draft_host?
|
|
57
|
+
ENV["PLEK_HOSTNAME_PREFIX"] == "draft-"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def force_graphql_param
|
|
61
|
+
return true if request.params["graphql"] == "true"
|
|
62
|
+
|
|
63
|
+
false if request.params["graphql"] == "false"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def graphql_schema_allowed?(schema_name)
|
|
67
|
+
Rails.application.config.graphql_allowed_schemas&.include?(schema_name) || false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def within_graphql_traffic_rate?(schema_name)
|
|
71
|
+
graphql_traffic_rate = Rails.application.config.graphql_traffic_rates&.dig(schema_name) || 0
|
|
72
|
+
Random.rand(1.0) < graphql_traffic_rate
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module GovukGraphqlTrafficRates
|
|
2
|
+
def self.configure
|
|
3
|
+
rates = graphql_rates_from_env
|
|
4
|
+
|
|
5
|
+
Rails.application.config.graphql_traffic_rates = rates
|
|
6
|
+
Rails.application.config.graphql_allowed_schemas = rates.keys
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.graphql_rates_from_env
|
|
10
|
+
ENV
|
|
11
|
+
.select { |key, _| key.start_with?("GRAPHQL_RATE_") }
|
|
12
|
+
.transform_keys { |key| key.delete_prefix("GRAPHQL_RATE_").downcase }
|
|
13
|
+
.transform_values(&:to_f)
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: govuk_content_item_loader
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GOV.UK Dev
|
|
@@ -23,6 +23,48 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '99.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: railties
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: climate_control
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: ostruct
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
26
68
|
- !ruby/object:Gem::Dependency
|
|
27
69
|
name: rails
|
|
28
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -117,6 +159,8 @@ files:
|
|
|
117
159
|
- Rakefile
|
|
118
160
|
- govuk_content_item_loader.gemspec
|
|
119
161
|
- lib/govuk_content_item_loader.rb
|
|
162
|
+
- lib/govuk_content_item_loader/govuk_conditional_content_item_loader.rb
|
|
163
|
+
- lib/govuk_content_item_loader/govuk_graphql_traffic_rates.rb
|
|
120
164
|
- lib/govuk_content_item_loader/version.rb
|
|
121
165
|
homepage: https://github.com/alphagov/govuk_content_item_loader
|
|
122
166
|
licenses:
|