Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configure Alloy to be use as a self-service tool for logs (source podlogs) #233

Merged
merged 80 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
668fc5a
add comments
TheoBrigitte Sep 26, 2024
58c41e5
user PodLogs for Kubernetes pods discovery
TheoBrigitte Sep 26, 2024
f79fa60
fix PodLogs label selector
TheoBrigitte Sep 26, 2024
2009c8a
fix PodLogs label selector
TheoBrigitte Sep 26, 2024
8b9a5ac
add PodLogs
TheoBrigitte Sep 26, 2024
192c954
remove PodLogs resources
TheoBrigitte Sep 26, 2024
f962a72
add promv1 prometheus-operator/pkg/apis/monitoring; go mod tidy
TheoBrigitte Sep 26, 2024
1cca88a
add podlogs.v1alpha2 definition
TheoBrigitte Sep 26, 2024
b9c0bba
register podlogs.v1alpha2 in client
TheoBrigitte Sep 26, 2024
063d626
add podlogs resource reconciler logic
TheoBrigitte Sep 26, 2024
bcd9b8e
address CI imports complain
TheoBrigitte Sep 26, 2024
5346e2c
wire podlogs resource
TheoBrigitte Sep 26, 2024
72cb704
Merge remote-tracking branch 'origin/main' into logs-self-service
TheoBrigitte Sep 26, 2024
7c17e02
add rbac rule for podlogs access
TheoBrigitte Sep 26, 2024
54c36a6
wire loki.source.podlogs with loki.relabel
TheoBrigitte Sep 26, 2024
cc0b5ac
better IgnoreNotFound
TheoBrigitte Sep 26, 2024
713da42
add script to update podlogs definition
TheoBrigitte Sep 26, 2024
e28a5b9
run podLogs resource right after logging
TheoBrigitte Sep 30, 2024
43d86af
use key.LoggingLabel
TheoBrigitte Sep 30, 2024
7ea20fb
print podlogs reconciliation result
TheoBrigitte Sep 30, 2024
e1a990c
add comment on podlogs sync script
TheoBrigitte Sep 30, 2024
6dccdc4
add by-namespace podlogs; add support for multiple podlogs
TheoBrigitte Sep 30, 2024
e0c987d
use podlogs as yaml resources
TheoBrigitte Sep 30, 2024
863f198
add podlog namespace
TheoBrigitte Sep 30, 2024
e8d0ce1
switch back to PodLogs in Go code
TheoBrigitte Sep 30, 2024
eedc11c
add kube-system podlogs
TheoBrigitte Sep 30, 2024
d7b2d0a
rm podlogs_pod.yaml
TheoBrigitte Sep 30, 2024
35c0c01
fix lastErr
TheoBrigitte Sep 30, 2024
625dfdf
debug labels
TheoBrigitte Sep 30, 2024
d2f9971
remove livedebugging
TheoBrigitte Sep 30, 2024
4117d06
use a single podlogs
TheoBrigitte Sep 30, 2024
88e0d40
go back to multiple podlogs
TheoBrigitte Oct 1, 2024
93e5d90
rename byPod byNamespace, podSelector namespaceSelector
TheoBrigitte Oct 1, 2024
b879d62
keep all targets on workload clusters
TheoBrigitte Oct 1, 2024
0f2715c
remove podlogs resources, use alloy values instead
TheoBrigitte Oct 1, 2024
9856a7d
select podlogs with giantswarm.io/managed-by=alloy-logs
TheoBrigitte Oct 1, 2024
3a9e48e
go mod tidy
TheoBrigitte Oct 3, 2024
366a172
Merge branch 'main' into logs-self-service
TheoBrigitte Oct 7, 2024
4ed5aa7
add giantswarm namespace; remove relabeling from default
TheoBrigitte Oct 7, 2024
4da255c
add giantswarm namespace; remove relabeling from default (in alloy co…
TheoBrigitte Oct 7, 2024
d7e8662
fix podlogs namespaceSelector
TheoBrigitte Oct 7, 2024
696a777
fix podselector matchExpressions operator DoesNotExist
TheoBrigitte Oct 7, 2024
b22f585
enable livedebugging
TheoBrigitte Oct 7, 2024
8514b7d
remove discovery.kubernetes
TheoBrigitte Oct 7, 2024
c571e08
remove tenant relabeling
TheoBrigitte Oct 7, 2024
5da34fd
remove podlogs resource
TheoBrigitte Oct 7, 2024
5d9f74a
remove podlogs schema registration
TheoBrigitte Oct 7, 2024
7411686
remove podlogs rbac
TheoBrigitte Oct 7, 2024
38c02c3
select enabled pods in disabled namespaces
TheoBrigitte Oct 7, 2024
94d71ca
select all pods on MCs
TheoBrigitte Oct 7, 2024
bf0f7ed
enable clustering
TheoBrigitte Oct 7, 2024
237b81e
add IsWorkloadCluster to alloyLoggingConfig
TheoBrigitte Oct 7, 2024
455c666
go mod tidy
TheoBrigitte Oct 7, 2024
575e6c0
update CHANGELOG
TheoBrigitte Oct 7, 2024
f50c982
make default workload cluster namespaces configurable
TheoBrigitte Oct 8, 2024
597cb02
move PodLogs to kube-system namespace
TheoBrigitte Oct 8, 2024
112f397
test array length
TheoBrigitte Oct 8, 2024
6287edb
move PodLogs to kube-system namespace
TheoBrigitte Oct 8, 2024
7112fc7
fix template empty string
TheoBrigitte Oct 8, 2024
e0645d9
remove default from Go code
TheoBrigitte Oct 8, 2024
03580ad
add defaultWorkloadClusterNamespaces to helm chart
TheoBrigitte Oct 8, 2024
84bc236
Merge branch 'main' into logs-self-service
TheoBrigitte Oct 8, 2024
d03e212
change giantswarm.io/logging label value to true/false; use key.Loggi…
TheoBrigitte Oct 8, 2024
2d1e358
generate different Alloy config for observability bundle v1.7.0 and v…
TheoBrigitte Oct 8, 2024
de42754
add Alloy config tests
TheoBrigitte Oct 8, 2024
fdcafd1
rename defaultWorkloadClusterNamespaces to defaultNamespaces
TheoBrigitte Oct 8, 2024
4327556
go mod tidy
TheoBrigitte Oct 8, 2024
396ea35
go fmt
TheoBrigitte Oct 8, 2024
fbc655f
use os instead of ioutil; ignore gosec on golden file permission
TheoBrigitte Oct 8, 2024
3a0bf07
test observability-bundle 1.6.2-b4af441ad0d6a2e1ee388eca3f96c5a4b22daf62
TheoBrigitte Oct 8, 2024
a58be62
test observability-bundle 1.6.2-d42c54cdac649244d06fd2c728d337658e958070
TheoBrigitte Oct 9, 2024
03906d5
no clustering on previous version
TheoBrigitte Oct 9, 2024
f0aeef9
update tests
TheoBrigitte Oct 9, 2024
c49c927
convert bool to string
TheoBrigitte Oct 9, 2024
93b21f9
use observability-bundle 1.7.0
TheoBrigitte Oct 9, 2024
2534b76
Merge branch 'main' into logs-self-service
TheoBrigitte Oct 9, 2024
a5d8dcd
fix EQ => GE
TheoBrigitte Oct 9, 2024
460d0ed
Update main.go
TheoBrigitte Oct 10, 2024
8e8433c
Merge branch 'main' into logs-self-service
TheoBrigitte Oct 10, 2024
ef0829c
update CHANGELOG
TheoBrigitte Oct 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add manual e2e testing procedure and script.
- [Alloy] Add capability to dynamically configure log targets using the `giantswarm.io/logging` label [#3618](https://github.com/giantswarm/roadmap/issues/3518)
- Changes Alloy config to use PodLogs for Kubernetes pods discovery.
- There is a performance impact on Kubernetes API
- Available from observability-bundle v17.0.0

### Changed

- [Alloy] Use PodLogs for Kubernetes pods discovery.
- [Alloy] Enable clustering
- Expose healthcheck port for kube-linter.

### Removed
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/giantswarm/logging-operator

go 1.22.0
go 1.23

toolchain go1.23.2

Expand All @@ -9,6 +9,7 @@ require (
github.com/blang/semver v3.5.1+incompatible
github.com/giantswarm/apiextensions-application v0.6.2
github.com/giantswarm/grafana-multi-tenant-proxy v0.8.0
github.com/google/go-cmp v0.6.0
github.com/onsi/ginkgo/v2 v2.20.2
github.com/onsi/gomega v1.34.2
github.com/pkg/errors v0.9.1
Expand Down Expand Up @@ -46,7 +47,6 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect
github.com/google/uuid v1.6.0 // indirect
Expand Down Expand Up @@ -84,7 +84,7 @@ require (
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/apiextensions-apiserver v0.31.0 // indirect
k8s.io/apiextensions-apiserver v0.31.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect
k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 // indirect
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -263,18 +263,18 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk=
k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk=
k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40=
k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ=
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/apiserver v0.31.0 h1:p+2dgJjy+bk+B1Csz+mc2wl5gHwvNkC9QJV+w55LVrY=
k8s.io/apiserver v0.31.0/go.mod h1:KI9ox5Yu902iBnnyMmy7ajonhKnkeZYJhTZ/YI+WEMk=
k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c=
k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM=
k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
k8s.io/cluster-bootstrap v0.30.3 h1:MgxyxMkpaC6mu0BKWJ8985XCOnKU+eH3Iy+biwtDXRk=
k8s.io/cluster-bootstrap v0.30.3/go.mod h1:h8BoLDfdD7XEEIXy7Bx9FcMzxHwz29jsYYi34bM5DKU=
k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs=
k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo=
k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8=
k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 h1:1dWzkmJrrprYvjGwh9kEUxmcUV/CtNU8QM7h1FLWQOo=
Expand Down
1 change: 1 addition & 0 deletions helm/logging-operator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ spec:
- -insecure-ca={{ .Values.managementCluster.insecureCA }}
- -installation-name={{ .Values.managementCluster.name }}
- -logging-agent={{ .Values.loggingOperator.loggingAgent }}
- -default-namespaces={{ .Values.loggingOperator.defaultNamespaces }}
livenessProbe:
httpGet:
path: /healthz
Expand Down
1 change: 1 addition & 0 deletions helm/logging-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ networkPolicy:
flavor: cilium

loggingOperator:
defaultNamespaces: "kube-system,giantswarm"
vintageMode: true
loggingEnabled: true
loggingAgent: promtail
Expand Down
17 changes: 16 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"flag"
"fmt"
"os"
"strings"

// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
Expand Down Expand Up @@ -65,7 +66,19 @@ func init() {
//+kubebuilder:scaffold:scheme
}

type StringSliceVar []string
TheoBrigitte marked this conversation as resolved.
Show resolved Hide resolved

func (s StringSliceVar) String() string {
return strings.Join(s, ",")
}

func (s *StringSliceVar) Set(value string) error {
*s = strings.Split(value, ",")
return nil
}

func main() {
var defaultNamespaces StringSliceVar
var enableLeaderElection bool
var enableLogging bool
var loggingAgent string
Expand All @@ -75,6 +88,7 @@ func main() {
var profilesAddr string
var probeAddr string
var vintageMode bool
flag.Var(&defaultNamespaces, "default-namespaces", "List of namespaces to collect logs from by default on workload clusters")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
Expand Down Expand Up @@ -147,7 +161,8 @@ func main() {
}

loggingConfig := loggingconfig.Reconciler{
Client: mgr.GetClient(),
Client: mgr.GetClient(),
DefaultWorkloadClusterNamespaces: defaultNamespaces,
}

grafanaAgentSecret := grafanaagentsecret.Reconciler{
Expand Down
30 changes: 23 additions & 7 deletions pkg/resource/logging-config/alloy-logging-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"text/template"

"github.com/Masterminds/sprig/v3"
"github.com/blang/semver"

"github.com/giantswarm/logging-operator/pkg/common"
"github.com/giantswarm/logging-operator/pkg/key"
loggedcluster "github.com/giantswarm/logging-operator/pkg/logged-cluster"
loggingsecret "github.com/giantswarm/logging-operator/pkg/resource/logging-secret"
)
Expand All @@ -20,6 +22,8 @@ var (
//go:embed alloy/logging-config.alloy.yaml.template
alloyLoggingConfig string
alloyLoggingConfigTemplate *template.Template

supportPodLogs = semver.MustParse("1.7.0")
)

func init() {
Expand All @@ -29,20 +33,29 @@ func init() {

// GenerateAlloyLoggingConfig returns a configmap for
// the logging extra-config
func GenerateAlloyLoggingConfig(lc loggedcluster.Interface) (string, error) {
func GenerateAlloyLoggingConfig(lc loggedcluster.Interface, observabilityBundleVersion semver.Version, defaultNamespaces []string) (string, error) {
var values bytes.Buffer

alloyConfig, err := generateAlloyConfig(lc)
alloyConfig, err := generateAlloyConfig(lc, observabilityBundleVersion)
if err != nil {
return "", err
}

data := struct {
AlloyConfig string
SecretName string
AlloyConfig string
DefaultWorkloadClusterNamespaces []string
IsWorkloadCluster bool
LoggingLabel string
SecretName string
SupportPodLogs bool
}{
AlloyConfig: alloyConfig,
SecretName: common.AlloyLogAgentAppName,
AlloyConfig: alloyConfig,
DefaultWorkloadClusterNamespaces: defaultNamespaces,
IsWorkloadCluster: common.IsWorkloadCluster(lc),
LoggingLabel: key.LoggingLabel,
SecretName: common.AlloyLogAgentAppName,
// Observability bundle in older versions do not support PodLogs
SupportPodLogs: observabilityBundleVersion.GE(supportPodLogs),
}

err = alloyLoggingConfigTemplate.Execute(&values, data)
Expand All @@ -53,7 +66,7 @@ func GenerateAlloyLoggingConfig(lc loggedcluster.Interface) (string, error) {
return values.String(), nil
}

func generateAlloyConfig(lc loggedcluster.Interface) (string, error) {
func generateAlloyConfig(lc loggedcluster.Interface, observabilityBundleVersion semver.Version) (string, error) {
var values bytes.Buffer

clusterName := lc.GetClusterName()
Expand All @@ -67,6 +80,7 @@ func generateAlloyConfig(lc loggedcluster.Interface) (string, error) {
TenantIDEnvVarName string
BasicAuthUsernameEnvVarName string
BasicAuthPasswordEnvVarName string
SupportPodLogs bool
}{
ClusterID: clusterName,
Installation: lc.GetInstallationName(),
Expand All @@ -76,6 +90,8 @@ func generateAlloyConfig(lc loggedcluster.Interface) (string, error) {
TenantIDEnvVarName: loggingsecret.AlloyTenantIDEnvVarName,
BasicAuthUsernameEnvVarName: loggingsecret.AlloyBasicAuthUsernameEnvVarName,
BasicAuthPasswordEnvVarName: loggingsecret.AlloyBasicAuthPasswordEnvVarName,
// Observability bundle in older versions do not support PodLogs
SupportPodLogs: observabilityBundleVersion.GE(supportPodLogs),
}

err := alloyLoggingTemplate.Execute(&values, data)
Expand Down
114 changes: 114 additions & 0 deletions pkg/resource/logging-config/alloy-logging-config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package loggingconfig

import (
TheoBrigitte marked this conversation as resolved.
Show resolved Hide resolved
_ "embed"
"flag"
"os"
"path/filepath"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
capi "sigs.k8s.io/cluster-api/api/v1beta1"

"github.com/blang/semver"
"github.com/google/go-cmp/cmp"

loggedcluster "github.com/giantswarm/logging-operator/pkg/logged-cluster"
"github.com/giantswarm/logging-operator/pkg/logged-cluster/capicluster"
)

var (
update = flag.Bool("update", false, "update .golden files")
)

func TestGenerateAlloyLoggingConfig(t *testing.T) {
testCases := []struct {
goldenFile string
observabilityBundleVersion string
defaultNamespaces []string
installationName string
clusterName string
}{
{
goldenFile: "alloy/test/logging-config.alloy.162_MC.yaml",
observabilityBundleVersion: "1.6.2",
installationName: "test-installation",
clusterName: "test-installation",
},
{
goldenFile: "alloy/test/logging-config.alloy.162_WC.yaml",
observabilityBundleVersion: "1.6.2",
installationName: "test-installation",
clusterName: "test-cluster",
},
{
goldenFile: "alloy/test/logging-config.alloy.170_MC.yaml",
observabilityBundleVersion: "1.7.0",
defaultNamespaces: []string{"test-selector"},
installationName: "test-installation",
clusterName: "test-installation",
},
{
goldenFile: "alloy/test/logging-config.alloy.170_WC.yaml",
TheoBrigitte marked this conversation as resolved.
Show resolved Hide resolved
observabilityBundleVersion: "1.7.0",
defaultNamespaces: []string{"test-selector"},
installationName: "test-installation",
clusterName: "test-cluster",
},
{
goldenFile: "alloy/test/logging-config.alloy.170_WC_default_namespaces_nil.yaml",
observabilityBundleVersion: "1.7.0",
defaultNamespaces: nil,
installationName: "test-installation",
clusterName: "test-cluster",
},
{
goldenFile: "alloy/test/logging-config.alloy.170_WC_default_namespaces_empty.yaml",
observabilityBundleVersion: "1.7.0",
defaultNamespaces: []string{""},
installationName: "test-installation",
clusterName: "test-cluster",
},
}

for _, tc := range testCases {
t.Run(filepath.Base(tc.goldenFile), func(t *testing.T) {
observabilityBundleVersion, err := semver.Parse(tc.observabilityBundleVersion)
if err != nil {
t.Fatalf("Failed to parse observability bundle version: %v", err)
}
golden, err := os.ReadFile(tc.goldenFile)
if err != nil {
t.Fatalf("Failed to read golden file: %v", err)
}

loggedCluster := &capicluster.Object{
Object: &capi.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: tc.clusterName,
},
},
Options: loggedcluster.Options{
InstallationName: tc.installationName,
},
}

config, err := GenerateAlloyLoggingConfig(loggedCluster, observabilityBundleVersion, tc.defaultNamespaces)
if err != nil {
t.Fatalf("Failed to generate alloy config: %v", err)
}

//fmt.Printf("=>> config\n%s", config)
//fmt.Printf("=>> config_v170_MC\n%s", config_v170_MC)
if !cmp.Equal(string(golden), config) {
t.Logf("Generated config differs from %s, diff:\n%s", tc.goldenFile, cmp.Diff(string(golden), config))
if *update {
//nolint:gosec
if err := os.WriteFile(tc.goldenFile, []byte(config), 0644); err != nil {
t.Fatalf("Failed to update golden file: %v", err)
}
}
}
})
}
}
Loading