feat: add audio features and sounds to the game #197

Merged
jleibl merged 3 commits from task/CAS-78/AddSoundEffects into main 2025-05-15 12:15:23 +00:00
12 changed files with 140 additions and 1 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,10 +1,12 @@
import { Component, HostListener, signal } from '@angular/core'; import { Component, HostListener, inject, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { NavbarComponent } from './shared/components/navbar/navbar.component'; import { NavbarComponent } from './shared/components/navbar/navbar.component';
import { FooterComponent } from './shared/components/footer/footer.component'; import { FooterComponent } from './shared/components/footer/footer.component';
import { LoginComponent } from './feature/auth/login/login.component'; import { LoginComponent } from './feature/auth/login/login.component';
import { RegisterComponent } from './feature/auth/register/register.component'; import { RegisterComponent } from './feature/auth/register/register.component';
import { RecoverPasswordComponent } from './feature/auth/recover-password/recover-password.component'; import { RecoverPasswordComponent } from './feature/auth/recover-password/recover-password.component';
import { PlaySoundDirective } from './shared/directives/play-sound.directive';
import { SoundInitializerService } from './shared/services/sound-initializer.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -16,14 +18,22 @@ import { RecoverPasswordComponent } from './feature/auth/recover-password/recove
LoginComponent, LoginComponent,
RegisterComponent, RegisterComponent,
RecoverPasswordComponent, RecoverPasswordComponent,
PlaySoundDirective,
], ],
templateUrl: './app.component.html', templateUrl: './app.component.html',
hostDirectives: [PlaySoundDirective],
}) })
export class AppComponent { export class AppComponent {
private soundInitializer = inject(SoundInitializerService);
showLogin = signal(false); showLogin = signal(false);
showRegister = signal(false); showRegister = signal(false);
showRecoverPassword = signal(false); showRecoverPassword = signal(false);
constructor() {
this.soundInitializer.initialize();
}
@HostListener('document:keydown.escape') @HostListener('document:keydown.escape')
handleEscapeKey() { handleEscapeKey() {
this.hideAuthForms(); this.hideAuthForms();

View file

@ -14,6 +14,7 @@ import { UserService } from '@service/user.service';
import { timer } from 'rxjs'; import { timer } from 'rxjs';
import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component'; import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component';
import { AuthService } from '@service/auth.service'; import { AuthService } from '@service/auth.service';
import { AudioService } from '@shared/services/audio.service';
@Component({ @Component({
selector: 'app-blackjack', selector: 'app-blackjack',
@ -35,6 +36,7 @@ export default class BlackjackComponent implements OnInit {
private userService = inject(UserService); private userService = inject(UserService);
private authService = inject(AuthService); private authService = inject(AuthService);
private blackjackService = inject(BlackjackService); private blackjackService = inject(BlackjackService);
private audioService = inject(AudioService);
dealerCards = signal<Card[]>([]); dealerCards = signal<Card[]>([]);
playerCards = signal<Card[]>([]); playerCards = signal<Card[]>([]);
@ -91,6 +93,9 @@ export default class BlackjackComponent implements OnInit {
// Show the result dialog after refreshing user data // Show the result dialog after refreshing user data
timer(500).subscribe(() => { timer(500).subscribe(() => {
this.showGameResult.set(true); this.showGameResult.set(true);
if (game.state === GameState.PLAYER_WON || game.state === GameState.PLAYER_BLACKJACK) {
this.audioService.playWinSound();
}
console.log('Game result dialog shown after delay'); console.log('Game result dialog shown after delay');
}); });
}); });
@ -99,6 +104,7 @@ export default class BlackjackComponent implements OnInit {
onNewGame(bet: number): void { onNewGame(bet: number): void {
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.audioService.playBetSound();
this.blackjackService.startGame(bet).subscribe({ this.blackjackService.startGame(bet).subscribe({
next: (game) => { next: (game) => {
@ -117,6 +123,7 @@ export default class BlackjackComponent implements OnInit {
if (!this.currentGameId() || this.isActionInProgress()) return; if (!this.currentGameId() || this.isActionInProgress()) return;
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.audioService.playBetSound();
this.blackjackService.hit(this.currentGameId()!).subscribe({ this.blackjackService.hit(this.currentGameId()!).subscribe({
next: (game) => { next: (game) => {
@ -143,6 +150,7 @@ export default class BlackjackComponent implements OnInit {
} }
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.audioService.playBetSound();
this.blackjackService.stand(this.currentGameId()!).subscribe({ this.blackjackService.stand(this.currentGameId()!).subscribe({
next: (game) => { next: (game) => {
@ -167,6 +175,7 @@ export default class BlackjackComponent implements OnInit {
} }
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.audioService.playBetSound();
this.blackjackService.doubleDown(this.currentGameId()!).subscribe({ this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
next: (game) => { next: (game) => {

View file

@ -40,6 +40,7 @@ export default class SlotsComponent implements OnInit, OnDestroy {
private userService = inject(UserService); private userService = inject(UserService);
private authService = inject(AuthService); private authService = inject(AuthService);
private userSubscription: Subscription | undefined; private userSubscription: Subscription | undefined;
private winSound: HTMLAudioElement;
slotInfo = signal<Record<string, number> | null>(null); slotInfo = signal<Record<string, number> | null>(null);
slotResult = signal<SlotResult>({ slotResult = signal<SlotResult>({
@ -56,6 +57,10 @@ export default class SlotsComponent implements OnInit, OnDestroy {
betAmount = signal<number>(1); betAmount = signal<number>(1);
isSpinning = false; isSpinning = false;
constructor() {
this.winSound = new Audio('/sounds/win.mp3');
}
ngOnInit(): void { ngOnInit(): void {
this.httpClient.get<Record<string, number>>('/backend/slots/info').subscribe((data) => { this.httpClient.get<Record<string, number>>('/backend/slots/info').subscribe((data) => {
this.slotInfo.set(data); this.slotInfo.set(data);
@ -111,6 +116,7 @@ export default class SlotsComponent implements OnInit, OnDestroy {
this.slotResult.set(result); this.slotResult.set(result);
if (result.status === 'win') { if (result.status === 'win') {
this.winSound.play();
this.userService.updateLocalBalance(result.amount); this.userService.updateLocalBalance(result.amount);
} }

View file

@ -24,6 +24,7 @@ export default class LootboxOpeningComponent {
prizeList: Reward[] = []; prizeList: Reward[] = [];
animationCompleted = false; animationCompleted = false;
currentUser: User | null = null; currentUser: User | null = null;
private winSound: HTMLAudioElement;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -33,6 +34,7 @@ export default class LootboxOpeningComponent {
private authService: AuthService, private authService: AuthService,
private cdr: ChangeDetectorRef private cdr: ChangeDetectorRef
) { ) {
this.winSound = new Audio('/sounds/win.mp3');
this.loadLootbox(); this.loadLootbox();
this.authService.userSubject.subscribe((user) => { this.authService.userSubject.subscribe((user) => {
this.currentUser = user; this.currentUser = user;
@ -145,6 +147,7 @@ export default class LootboxOpeningComponent {
this.animationCompleted = true; this.animationCompleted = true;
if (this.wonReward) { if (this.wonReward) {
this.winSound.play();
this.userService.updateLocalBalance(this.wonReward.value); this.userService.updateLocalBalance(this.wonReward.value);
} }

View file

@ -0,0 +1,15 @@
import { Directive, HostListener, inject } from '@angular/core';
import { AudioService } from '../services/audio.service';
@Directive({
selector: '[appPlaySound]',
standalone: true,
})
export class PlaySoundDirective {
private audioService = inject(AudioService);
@HostListener('click')
onClick() {
this.audioService.playBetSound();
}
}

View file

@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class AudioService {
private audioCache = new Map<string, HTMLAudioElement>();
private getAudio(soundName: string): HTMLAudioElement {
if (this.audioCache.has(soundName)) {
ptran marked this conversation as resolved Outdated

flip this if statement for better readability

flip this if statement for better readability

`package main

import (
"context"
"crypto/x509"
"encoding/json"
"fmt"
"io"
stdlog "log"
"maps"
"net/http"
"os"
"os/signal"
"slices"
"strings"
"syscall"
"time"

"github.com/coreos/go-systemd/v22/daemon"
"github.com/go-acme/lego/v4/challenge"
gokitmetrics "github.com/go-kit/kit/metrics"
"github.com/rs/zerolog/log"
"github.com/sirupsen/logrus"
"github.com/spiffe/go-spiffe/v2/workloadapi"
"github.com/traefik/paerser/cli"
"github.com/traefik/traefik/v3/cmd"
"github.com/traefik/traefik/v3/cmd/healthcheck"
cmdVersion "github.com/traefik/traefik/v3/cmd/version"
tcli "github.com/traefik/traefik/v3/pkg/cli"
"github.com/traefik/traefik/v3/pkg/collector"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/runtime"
"github.com/traefik/traefik/v3/pkg/config/static"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/provider/acme"
"github.com/traefik/traefik/v3/pkg/provider/aggregator"
"github.com/traefik/traefik/v3/pkg/provider/tailscale"
"github.com/traefik/traefik/v3/pkg/provider/traefik"
"github.com/traefik/traefik/v3/pkg/proxy"
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
"github.com/traefik/traefik/v3/pkg/safe"
"github.com/traefik/traefik/v3/pkg/server"
"github.com/traefik/traefik/v3/pkg/server/middleware"
"github.com/traefik/traefik/v3/pkg/server/service"
"github.com/traefik/traefik/v3/pkg/tcp"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/traefik/traefik/v3/pkg/types"
"github.com/traefik/traefik/v3/pkg/version"

)

func main() {
// traefik config inits
tConfig := cmd.NewTraefikConfiguration()

loaders := []cli.ResourceLoader{&tcli.DeprecationLoader{}, &tcli.FileLoader{}, &tcli.FlagLoader{}, &tcli.EnvLoader{}}

cmdTraefik := &cli.Command{
	Name: "traefik",
	Description: `Traefik is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.

Complete documentation is available at https://traefik.io`,
Configuration: tConfig,
Resources: loaders,
Run: func(_ []string) error {
return runCmd(&tConfig.Configuration)
},
}

err := cmdTraefik.AddCommand(healthcheck.NewCmd(&tConfig.Configuration, loaders))
if err != nil {
	stdlog.Println(err)
	os.Exit(1)
}

err = cmdTraefik.AddCommand(cmdVersion.NewCmd())
if err != nil {
	stdlog.Println(err)
	os.Exit(1)
}

err = cli.Execute(cmdTraefik)
if err != nil {
	log.Error().Err(err).Msg("Command error")
	logrus.Exit(1)
}

logrus.Exit(0)

}

func runCmd(staticConfiguration *static.Configuration) error {
if err := setupLogger(staticConfiguration); err != nil {
return fmt.Errorf("setting up logger: %w", err)
}

http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment

staticConfiguration.SetEffectiveConfiguration()
if err := staticConfiguration.ValidateConfiguration(); err != nil {
	return err
}

log.Info().Str("version", version.Version).
	Msgf("Traefik version %s built on %s", version.Version, version.BuildDate)

jsonConf, err := json.Marshal(staticConfiguration)
if err != nil {
	log.Error().Err(err).Msg("Could not marshal static configuration")
	log.Debug().Interface("staticConfiguration", staticConfiguration).Msg("Static configuration loaded [struct]")
} else {
	log.Debug().RawJSON("staticConfiguration", jsonConf).Msg("Static configuration loaded [json]")
}

if staticConfiguration.Global.CheckNewVersion {
	checkNewVersion()
}

stats(staticConfiguration)

svr, err := setupServer(staticConfiguration)
if err != nil {
	return err
}

ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)

if staticConfiguration.Ping != nil {
	staticConfiguration.Ping.WithContext(ctx)
}

svr.Start(ctx)
defer svr.Close()

sent, err := daemon.SdNotify(false, "READY=1")
if !sent && err != nil {
	log.Error().Err(err).Msg("Failed to notify")
}

t, err := daemon.SdWatchdogEnabled(false)
if err != nil {
	log.Error().Err(err).Msg("Could not enable Watchdog")
} else if t != 0 {
	// Send a ping each half time given
	t /= 2
	log.Info().Msgf("Watchdog activated with timer duration %s", t)
	safe.Go(func() {
		tick := time.Tick(t)
		for range tick {
			resp, errHealthCheck := healthcheck.Do(*staticConfiguration)
			if resp != nil {
				_ = resp.Body.Close()
			}

			if staticConfiguration.Ping == nil || errHealthCheck == nil {
				if ok, _ := daemon.SdNotify(false, "WATCHDOG=1"); !ok {
					log.Error().Msg("Fail to tick watchdog")
				}
			} else {
				log.Error().Err(errHealthCheck).Send()
			}
		}
	})
}

svr.Wait()
log.Info().Msg("Shutting down")
return nil

}

func setupServer(staticConfiguration *static.Configuration) (*server.Server, error) {
providerAggregator := aggregator.NewProviderAggregator(*staticConfiguration.Providers)

ctx := context.Background()
routinesPool := safe.NewPool(ctx)

// adds internal provider
err := providerAggregator.AddProvider(traefik.New(*staticConfiguration))
if err != nil {
	return nil, err
}

// ACME

tlsManager := traefiktls.NewManager()
httpChallengeProvider := acme.NewChallengeHTTP()

tlsChallengeProvider := acme.NewChallengeTLSALPN()
err = providerAggregator.AddProvider(tlsChallengeProvider)
if err != nil {
	return nil, err
}

acmeProviders := initACMEProvider(staticConfiguration, providerAggregator, tlsManager, httpChallengeProvider, tlsChallengeProvider, routinesPool)

// Tailscale

tsProviders := initTailscaleProviders(staticConfiguration, providerAggregator)

// Observability

metricRegistries := registerMetricClients(staticConfiguration.Metrics)
var semConvMetricRegistry *metrics.SemConvMetricsRegistry
if staticConfiguration.Metrics != nil && staticConfiguration.Metrics.OTLP != nil {
	semConvMetricRegistry, err = metrics.NewSemConvMetricRegistry(ctx, staticConfiguration.Metrics.OTLP)
	if err != nil {
		return nil, fmt.Errorf("unable to create SemConv metric registry: %w", err)
	}
}
metricsRegistry := metrics.NewMultiRegistry(metricRegistries)
accessLog := setupAccessLog(staticConfiguration.AccessLog)
tracer, tracerCloser := setupTracing(staticConfiguration.Tracing)
observabilityMgr := middleware.NewObservabilityMgr(*staticConfiguration, metricsRegistry, semConvMetricRegistry, accessLog, tracer, tracerCloser)

// Entrypoints

serverEntryPointsTCP, err := server.NewTCPEntryPoints(staticConfiguration.EntryPoints, staticConfiguration.HostResolver, metricsRegistry)
if err != nil {
	return nil, err
}

serverEntryPointsUDP, err := server.NewUDPEntryPoints(staticConfiguration.EntryPoints)
if err != nil {
	return nil, err
}

if staticConfiguration.API != nil {
	version.DisableDashboardAd = staticConfiguration.API.DisableDashboardAd
}

// Plugins
pluginLogger := log.Ctx(ctx).With().Logger()
hasPlugins := staticConfiguration.Experimental != nil && (staticConfiguration.Experimental.Plugins != nil || staticConfiguration.Experimental.LocalPlugins != nil)
if hasPlugins {
	pluginsList := slices.Collect(maps.Keys(staticConfiguration.Experimental.Plugins))
	pluginsList = append(pluginsList, slices.Collect(maps.Keys(staticConfiguration.Experimental.LocalPlugins))...)

	pluginLogger = pluginLogger.With().Strs("plugins", pluginsList).Logger()
	pluginLogger.Info().Msg("Loading plugins...")
}

pluginBuilder, err := createPluginBuilder(staticConfiguration)
if err != nil && staticConfiguration.Experimental != nil && staticConfiguration.Experimental.AbortOnPluginFailure {
	return nil, fmt.Errorf("plugin: failed to create plugin builder: %w", err)
}
if err != nil {
	pluginLogger.Err(err).Msg("Plugins are disabled because an error has occurred.")
} else if hasPlugins {
	pluginLogger.Info().Msg("Plugins loaded.")
}

// Providers plugins

for name, conf := range staticConfiguration.Providers.Plugin {
	if pluginBuilder == nil {
		break
	}

	p, err := pluginBuilder.BuildProvider(name, conf)
	if err != nil {
		return nil, fmt.Errorf("plugin: failed to build provider: %w", err)
	}

	err = providerAggregator.AddProvider(p)
	if err != nil {
		return nil, fmt.Errorf("plugin: failed to add provider: %w", err)
	}
}

// Service manager factory

var spiffeX509Source *workloadapi.X509Source
if staticConfiguration.Spiffe != nil && staticConfiguration.Spiffe.WorkloadAPIAddr != "" {
	log.Info().Str("workloadAPIAddr", staticConfiguration.Spiffe.WorkloadAPIAddr).
		Msg("Waiting on SPIFFE SVID delivery")

	spiffeX509Source, err = workloadapi.NewX509Source(
		ctx,
		workloadapi.WithClientOptions(
			workloadapi.WithAddr(
				staticConfiguration.Spiffe.WorkloadAPIAddr,
			),
		),
	)
	if err != nil {
		return nil, fmt.Errorf("unable to create SPIFFE x509 source: %w", err)
	}
	log.Info().Msg("Successfully obtained SPIFFE SVID.")
}

transportManager := service.NewTransportManager(spiffeX509Source)

var proxyBuilder service.ProxyBuilder = httputil.NewProxyBuilder(transportManager, semConvMetricRegistry)
if staticConfiguration.Experimental != nil && staticConfiguration.Experimental.FastProxy != nil {
	proxyBuilder = proxy.NewSmartBuilder(transportManager, proxyBuilder, *staticConfiguration.Experimental.FastProxy)
}

dialerManager := tcp.NewDialerManager(spiffeX509Source)
acmeHTTPHandler := getHTTPChallengeHandler(acmeProviders, httpChallengeProvider)
managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, observabilityMgr, transportManager, proxyBuilder, acmeHTTPHandler)

// Router factory

routerFactory := server.NewRouterFactory(*staticConfiguration, managerFactory, tlsManager, observabilityMgr, pluginBuilder, dialerManager)

// Watcher

watcher := server.NewConfigurationWatcher(
	routinesPool,
	providerAggregator,
	getDefaultsEntrypoints(staticConfiguration),
	"internal",
)

// TLS
watcher.AddListener(func(conf dynamic.Configuration) {
	ctx := context.Background()
	tlsManager.UpdateConfigs(ctx, conf.TLS.Stores, conf.TLS.Options, conf.TLS.Certificates)

	gauge := metricsRegistry.TLSCertsNotAfterTimestampGauge()
	for _, certificate := range tlsManager.GetServerCertificates() {
		appendCertMetric(gauge, certificate)
	}
})

// Metrics
watcher.AddListener(func(_ dynamic.Configuration) {
	metricsRegistry.ConfigReloadsCounter().Add(1)
	metricsRegistry.LastConfigReloadSuccessGauge().Set(float64(time.Now().Unix()))
})

// Server Transports
watcher.AddListener(func(conf dynamic.Configuration) {
	transportManager.Update(conf.HTTP.ServersTransports)
	proxyBuilder.Update(conf.HTTP.ServersTransports)
	dialerManager.Update(conf.TCP.ServersTransports)
})

// Switch router
watcher.AddListener(switchRouter(routerFactory, serverEntryPointsTCP, serverEntryPointsUDP))

// Metrics
if metricsRegistry.IsEpEnabled() || metricsRegistry.IsRouterEnabled() || metricsRegistry.IsSvcEnabled() {
	var eps []string
	for key := range serverEntryPointsTCP {
		eps = append(eps, key)
	}
	watcher.AddListener(func(conf dynamic.Configuration) {
		metrics.OnConfigurationUpdate(conf, eps)
	})
}

// TLS challenge
watcher.AddListener(tlsChallengeProvider.ListenConfiguration)

// Certificate Resolvers

resolverNames := map[string]struct{}{}

// ACME
for _, p := range acmeProviders {
	resolverNames[p.ResolverName] = struct{}{}
	watcher.AddListener(p.ListenConfiguration)
}

// Tailscale
for _, p := range tsProviders {
	resolverNames[p.ResolverName] = struct{}{}
	watcher.AddListener(p.HandleConfigUpdate)
}

// Certificate resolver logs
watcher.AddListener(func(config dynamic.Configuration) {
	for rtName, rt := range config.HTTP.Routers {
		if rt.TLS == nil || rt.TLS.CertResolver == "" {
			continue
		}

		if _, ok := resolverNames[rt.TLS.CertResolver]; !ok {
			log.Error().Err(err).Str(logs.RouterName, rtName).Str("certificateResolver", rt.TLS.CertResolver).
				Msg("Router uses a nonexistent certificate resolver")
		}
	}
})

return server.NewServer(routinesPool, serverEntryPointsTCP, serverEntryPointsUDP, watcher, observabilityMgr), nil

}

func getHTTPChallengeHandler(acmeProviders []*acme.Provider, httpChallengeProvider http.Handler) http.Handler {
var acmeHTTPHandler http.Handler
for _, p := range acmeProviders {
if p != nil && p.HTTPChallenge != nil {
acmeHTTPHandler = httpChallengeProvider
break
}
}
return acmeHTTPHandler
}

func getDefaultsEntrypoints(staticConfiguration *static.Configuration) []string {
var defaultEntryPoints []string

// Determines if at least one EntryPoint is configured to be used by default.
var hasDefinedDefaults bool
for _, ep := range staticConfiguration.EntryPoints {
	if ep.AsDefault {
		hasDefinedDefaults = true
		break
	}
}

for name, cfg := range staticConfiguration.EntryPoints {
	// By default all entrypoints are considered.
	// If at least one is flagged, then only flagged entrypoints are included.
	if hasDefinedDefaults && !cfg.AsDefault {
		continue
	}

	protocol, err := cfg.GetProtocol()
	if err != nil {
		// Should never happen because Traefik should not start if protocol is invalid.
		log.Error().Err(err).Msg("Invalid protocol")
	}

	if protocol != "udp" && name != static.DefaultInternalEntryPointName {
		defaultEntryPoints = append(defaultEntryPoints, name)
	}
}

slices.Sort(defaultEntryPoints)
return defaultEntryPoints

}

func switchRouter(routerFactory *server.RouterFactory, serverEntryPointsTCP server.TCPEntryPoints, serverEntryPointsUDP server.UDPEntryPoints) func(conf dynamic.Configuration) {
return func(conf dynamic.Configuration) {
rtConf := runtime.NewConfig(conf)

	routers, udpRouters := routerFactory.CreateRouters(rtConf)

	serverEntryPointsTCP.Switch(routers)
	serverEntryPointsUDP.Switch(udpRouters)
}

}

// initACMEProvider creates and registers acme.Provider instances corresponding to the configured ACME certificate resolvers.
func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.ProviderAggregator, tlsManager *traefiktls.Manager, httpChallengeProvider, tlsChallengeProvider challenge.Provider, routinesPool *safe.Pool) []*acme.Provider {
localStores := map[string]*acme.LocalStore{}

var resolvers []*acme.Provider
for name, resolver := range c.CertificatesResolvers {
	if resolver.ACME == nil {
		continue
	}

	if localStores[resolver.ACME.Storage] == nil {
		localStores[resolver.ACME.Storage] = acme.NewLocalStore(resolver.ACME.Storage, routinesPool)
	}

	p := &acme.Provider{
		Configuration:         resolver.ACME,
		Store:                 localStores[resolver.ACME.Storage],
		ResolverName:          name,
		HTTPChallengeProvider: httpChallengeProvider,
		TLSChallengeProvider:  tlsChallengeProvider,
	}

	if err := providerAggregator.AddProvider(p); err != nil {
		log.Error().Err(err).Str("resolver", name).Msg("The ACME resolve is skipped from the resolvers list")
		continue
	}

	p.SetTLSManager(tlsManager)

	p.SetConfigListenerChan(make(chan dynamic.Configuration))

	resolvers = append(resolvers, p)
}

return resolvers

}

// initTailscaleProviders creates and registers tailscale.Provider instances corresponding to the configured Tailscale certificate resolvers.
func initTailscaleProviders(cfg *static.Configuration, providerAggregator *aggregator.ProviderAggregator) []*tailscale.Provider {
var providers []*tailscale.Provider
for name, resolver := range cfg.CertificatesResolvers {
if resolver.Tailscale == nil {
continue
}

	tsProvider := &tailscale.Provider{ResolverName: name}

	if err := providerAggregator.AddProvider(tsProvider); err != nil {
		log.Error().Err(err).Str(logs.ProviderName, name).Msg("Unable to create Tailscale provider")
		continue
	}

	providers = append(providers, tsProvider)
}

return providers

}

func registerMetricClients(metricsConfig *types.Metrics) []metrics.Registry {
if metricsConfig == nil {
return nil
}

var registries []metrics.Registry

if metricsConfig.Prometheus != nil {
	logger := log.With().Str(logs.MetricsProviderName, "prometheus").Logger()

	prometheusRegister := metrics.RegisterPrometheus(logger.WithContext(context.Background()), metricsConfig.Prometheus)
	if prometheusRegister != nil {
		registries = append(registries, prometheusRegister)
		logger.Debug().Msg("Configured Prometheus metrics")
	}
}

if metricsConfig.Datadog != nil {
	logger := log.With().Str(logs.MetricsProviderName, "datadog").Logger()

	registries = append(registries, metrics.RegisterDatadog(logger.WithContext(context.Background()), metricsConfig.Datadog))
	logger.Debug().
		Str("address", metricsConfig.Datadog.Address).
		Str("pushInterval", metricsConfig.Datadog.PushInterval.String()).
		Msgf("Configured Datadog metrics")
}

if metricsConfig.StatsD != nil {
	logger := log.With().Str(logs.MetricsProviderName, "statsd").Logger()

	registries = append(registries, metrics.RegisterStatsd(logger.WithContext(context.Background()), metricsConfig.StatsD))
	logger.Debug().
		Str("address", metricsConfig.StatsD.Address).
		Str("pushInterval", metricsConfig.StatsD.PushInterval.String()).
		Msg("Configured StatsD metrics")
}

if metricsConfig.InfluxDB2 != nil {
	logger := log.With().Str(logs.MetricsProviderName, "influxdb2").Logger()

	influxDB2Register := metrics.RegisterInfluxDB2(logger.WithContext(context.Background()), metricsConfig.InfluxDB2)
	if influxDB2Register != nil {
		registries = append(registries, influxDB2Register)
		logger.Debug().
			Str("address", metricsConfig.InfluxDB2.Address).
			Str("bucket", metricsConfig.InfluxDB2.Bucket).
			Str("organization", metricsConfig.InfluxDB2.Org).
			Str("pushInterval", metricsConfig.InfluxDB2.PushInterval.String()).
			Msg("Configured InfluxDB v2 metrics")
	}
}

if metricsConfig.OTLP != nil {
	logger := log.With().Str(logs.MetricsProviderName, "openTelemetry").Logger()

	openTelemetryRegistry := metrics.RegisterOpenTelemetry(logger.WithContext(context.Background()), metricsConfig.OTLP)
	if openTelemetryRegistry != nil {
		registries = append(registries, openTelemetryRegistry)
		logger.Debug().
			Str("pushInterval", metricsConfig.OTLP.PushInterval.String()).
			Msg("Configured OpenTelemetry metrics")
	}
}

return registries

}

func appendCertMetric(gauge gokitmetrics.Gauge, certificate *x509.Certificate) {
slices.Sort(certificate.DNSNames)

labels := []string{
	"cn", certificate.Subject.CommonName,
	"serial", certificate.SerialNumber.String(),
	"sans", strings.Join(certificate.DNSNames, ","),
}

notAfter := float64(certificate.NotAfter.Unix())

gauge.With(labels...).Set(notAfter)

}

func setupAccessLog(conf *types.AccessLog) *accesslog.Handler {
if conf == nil {
return nil
}

accessLoggerMiddleware, err := accesslog.NewHandler(conf)
if err != nil {
	log.Warn().Err(err).Msg("Unable to create access logger")
	return nil
}

return accessLoggerMiddleware

}

func setupTracing(conf *static.Tracing) (*tracing.Tracer, io.Closer) {
if conf == nil {
return nil, nil
}

tracer, closer, err := tracing.NewTracing(conf)
if err != nil {
	log.Warn().Err(err).Msg("Unable to create tracer")
	return nil, nil
}

return tracer, closer

}

func checkNewVersion() {
ticker := time.Tick(24 * time.Hour)
safe.Go(func() {
for time.Sleep(10 * time.Minute); ; <-ticker {
version.CheckNewVersion()
}
})
}

func stats(staticConfiguration *static.Configuration) {
logger := log.With().Logger()

if staticConfiguration.Global.SendAnonymousUsage {
	logger.Info().Msg(`Stats collection is enabled.`)
	logger.Info().Msg(`Many thanks for contributing to Traefik's improvement by allowing us to receive anonymous information from your configuration.`)
	logger.Info().Msg(`Help us improve Traefik by leaving this feature on :)`)
	logger.Info().Msg(`More details on: https://doc.traefik.io/traefik/contributing/data-collection/`)
	collect(staticConfiguration)
} else {
	logger.Info().Msg(`

Stats collection is disabled.
Help us improve Traefik by turning this feature on :)
More details on: https://doc.traefik.io/traefik/contributing/data-collection/
`)
}
}

func collect(staticConfiguration *static.Configuration) {
ticker := time.Tick(24 * time.Hour)
safe.Go(func() {
for time.Sleep(10 * time.Minute); ; <-ticker {
if err := collector.Collect(staticConfiguration); err != nil {
log.Debug().Err(err).Send()
}
}
})
}
`

`package main import ( "context" "crypto/x509" "encoding/json" "fmt" "io" stdlog "log" "maps" "net/http" "os" "os/signal" "slices" "strings" "syscall" "time" "github.com/coreos/go-systemd/v22/daemon" "github.com/go-acme/lego/v4/challenge" gokitmetrics "github.com/go-kit/kit/metrics" "github.com/rs/zerolog/log" "github.com/sirupsen/logrus" "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/traefik/paerser/cli" "github.com/traefik/traefik/v3/cmd" "github.com/traefik/traefik/v3/cmd/healthcheck" cmdVersion "github.com/traefik/traefik/v3/cmd/version" tcli "github.com/traefik/traefik/v3/pkg/cli" "github.com/traefik/traefik/v3/pkg/collector" "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/config/runtime" "github.com/traefik/traefik/v3/pkg/config/static" "github.com/traefik/traefik/v3/pkg/logs" "github.com/traefik/traefik/v3/pkg/metrics" "github.com/traefik/traefik/v3/pkg/middlewares/accesslog" "github.com/traefik/traefik/v3/pkg/provider/acme" "github.com/traefik/traefik/v3/pkg/provider/aggregator" "github.com/traefik/traefik/v3/pkg/provider/tailscale" "github.com/traefik/traefik/v3/pkg/provider/traefik" "github.com/traefik/traefik/v3/pkg/proxy" "github.com/traefik/traefik/v3/pkg/proxy/httputil" "github.com/traefik/traefik/v3/pkg/safe" "github.com/traefik/traefik/v3/pkg/server" "github.com/traefik/traefik/v3/pkg/server/middleware" "github.com/traefik/traefik/v3/pkg/server/service" "github.com/traefik/traefik/v3/pkg/tcp" traefiktls "github.com/traefik/traefik/v3/pkg/tls" "github.com/traefik/traefik/v3/pkg/tracing" "github.com/traefik/traefik/v3/pkg/types" "github.com/traefik/traefik/v3/pkg/version" ) func main() { // traefik config inits tConfig := cmd.NewTraefikConfiguration() loaders := []cli.ResourceLoader{&tcli.DeprecationLoader{}, &tcli.FileLoader{}, &tcli.FlagLoader{}, &tcli.EnvLoader{}} cmdTraefik := &cli.Command{ Name: "traefik", Description: `Traefik is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease. Complete documentation is available at https://traefik.io`, Configuration: tConfig, Resources: loaders, Run: func(_ []string) error { return runCmd(&tConfig.Configuration) }, } err := cmdTraefik.AddCommand(healthcheck.NewCmd(&tConfig.Configuration, loaders)) if err != nil { stdlog.Println(err) os.Exit(1) } err = cmdTraefik.AddCommand(cmdVersion.NewCmd()) if err != nil { stdlog.Println(err) os.Exit(1) } err = cli.Execute(cmdTraefik) if err != nil { log.Error().Err(err).Msg("Command error") logrus.Exit(1) } logrus.Exit(0) } func runCmd(staticConfiguration *static.Configuration) error { if err := setupLogger(staticConfiguration); err != nil { return fmt.Errorf("setting up logger: %w", err) } http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment staticConfiguration.SetEffectiveConfiguration() if err := staticConfiguration.ValidateConfiguration(); err != nil { return err } log.Info().Str("version", version.Version). Msgf("Traefik version %s built on %s", version.Version, version.BuildDate) jsonConf, err := json.Marshal(staticConfiguration) if err != nil { log.Error().Err(err).Msg("Could not marshal static configuration") log.Debug().Interface("staticConfiguration", staticConfiguration).Msg("Static configuration loaded [struct]") } else { log.Debug().RawJSON("staticConfiguration", jsonConf).Msg("Static configuration loaded [json]") } if staticConfiguration.Global.CheckNewVersion { checkNewVersion() } stats(staticConfiguration) svr, err := setupServer(staticConfiguration) if err != nil { return err } ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) if staticConfiguration.Ping != nil { staticConfiguration.Ping.WithContext(ctx) } svr.Start(ctx) defer svr.Close() sent, err := daemon.SdNotify(false, "READY=1") if !sent && err != nil { log.Error().Err(err).Msg("Failed to notify") } t, err := daemon.SdWatchdogEnabled(false) if err != nil { log.Error().Err(err).Msg("Could not enable Watchdog") } else if t != 0 { // Send a ping each half time given t /= 2 log.Info().Msgf("Watchdog activated with timer duration %s", t) safe.Go(func() { tick := time.Tick(t) for range tick { resp, errHealthCheck := healthcheck.Do(*staticConfiguration) if resp != nil { _ = resp.Body.Close() } if staticConfiguration.Ping == nil || errHealthCheck == nil { if ok, _ := daemon.SdNotify(false, "WATCHDOG=1"); !ok { log.Error().Msg("Fail to tick watchdog") } } else { log.Error().Err(errHealthCheck).Send() } } }) } svr.Wait() log.Info().Msg("Shutting down") return nil } func setupServer(staticConfiguration *static.Configuration) (*server.Server, error) { providerAggregator := aggregator.NewProviderAggregator(*staticConfiguration.Providers) ctx := context.Background() routinesPool := safe.NewPool(ctx) // adds internal provider err := providerAggregator.AddProvider(traefik.New(*staticConfiguration)) if err != nil { return nil, err } // ACME tlsManager := traefiktls.NewManager() httpChallengeProvider := acme.NewChallengeHTTP() tlsChallengeProvider := acme.NewChallengeTLSALPN() err = providerAggregator.AddProvider(tlsChallengeProvider) if err != nil { return nil, err } acmeProviders := initACMEProvider(staticConfiguration, providerAggregator, tlsManager, httpChallengeProvider, tlsChallengeProvider, routinesPool) // Tailscale tsProviders := initTailscaleProviders(staticConfiguration, providerAggregator) // Observability metricRegistries := registerMetricClients(staticConfiguration.Metrics) var semConvMetricRegistry *metrics.SemConvMetricsRegistry if staticConfiguration.Metrics != nil && staticConfiguration.Metrics.OTLP != nil { semConvMetricRegistry, err = metrics.NewSemConvMetricRegistry(ctx, staticConfiguration.Metrics.OTLP) if err != nil { return nil, fmt.Errorf("unable to create SemConv metric registry: %w", err) } } metricsRegistry := metrics.NewMultiRegistry(metricRegistries) accessLog := setupAccessLog(staticConfiguration.AccessLog) tracer, tracerCloser := setupTracing(staticConfiguration.Tracing) observabilityMgr := middleware.NewObservabilityMgr(*staticConfiguration, metricsRegistry, semConvMetricRegistry, accessLog, tracer, tracerCloser) // Entrypoints serverEntryPointsTCP, err := server.NewTCPEntryPoints(staticConfiguration.EntryPoints, staticConfiguration.HostResolver, metricsRegistry) if err != nil { return nil, err } serverEntryPointsUDP, err := server.NewUDPEntryPoints(staticConfiguration.EntryPoints) if err != nil { return nil, err } if staticConfiguration.API != nil { version.DisableDashboardAd = staticConfiguration.API.DisableDashboardAd } // Plugins pluginLogger := log.Ctx(ctx).With().Logger() hasPlugins := staticConfiguration.Experimental != nil && (staticConfiguration.Experimental.Plugins != nil || staticConfiguration.Experimental.LocalPlugins != nil) if hasPlugins { pluginsList := slices.Collect(maps.Keys(staticConfiguration.Experimental.Plugins)) pluginsList = append(pluginsList, slices.Collect(maps.Keys(staticConfiguration.Experimental.LocalPlugins))...) pluginLogger = pluginLogger.With().Strs("plugins", pluginsList).Logger() pluginLogger.Info().Msg("Loading plugins...") } pluginBuilder, err := createPluginBuilder(staticConfiguration) if err != nil && staticConfiguration.Experimental != nil && staticConfiguration.Experimental.AbortOnPluginFailure { return nil, fmt.Errorf("plugin: failed to create plugin builder: %w", err) } if err != nil { pluginLogger.Err(err).Msg("Plugins are disabled because an error has occurred.") } else if hasPlugins { pluginLogger.Info().Msg("Plugins loaded.") } // Providers plugins for name, conf := range staticConfiguration.Providers.Plugin { if pluginBuilder == nil { break } p, err := pluginBuilder.BuildProvider(name, conf) if err != nil { return nil, fmt.Errorf("plugin: failed to build provider: %w", err) } err = providerAggregator.AddProvider(p) if err != nil { return nil, fmt.Errorf("plugin: failed to add provider: %w", err) } } // Service manager factory var spiffeX509Source *workloadapi.X509Source if staticConfiguration.Spiffe != nil && staticConfiguration.Spiffe.WorkloadAPIAddr != "" { log.Info().Str("workloadAPIAddr", staticConfiguration.Spiffe.WorkloadAPIAddr). Msg("Waiting on SPIFFE SVID delivery") spiffeX509Source, err = workloadapi.NewX509Source( ctx, workloadapi.WithClientOptions( workloadapi.WithAddr( staticConfiguration.Spiffe.WorkloadAPIAddr, ), ), ) if err != nil { return nil, fmt.Errorf("unable to create SPIFFE x509 source: %w", err) } log.Info().Msg("Successfully obtained SPIFFE SVID.") } transportManager := service.NewTransportManager(spiffeX509Source) var proxyBuilder service.ProxyBuilder = httputil.NewProxyBuilder(transportManager, semConvMetricRegistry) if staticConfiguration.Experimental != nil && staticConfiguration.Experimental.FastProxy != nil { proxyBuilder = proxy.NewSmartBuilder(transportManager, proxyBuilder, *staticConfiguration.Experimental.FastProxy) } dialerManager := tcp.NewDialerManager(spiffeX509Source) acmeHTTPHandler := getHTTPChallengeHandler(acmeProviders, httpChallengeProvider) managerFactory := service.NewManagerFactory(*staticConfiguration, routinesPool, observabilityMgr, transportManager, proxyBuilder, acmeHTTPHandler) // Router factory routerFactory := server.NewRouterFactory(*staticConfiguration, managerFactory, tlsManager, observabilityMgr, pluginBuilder, dialerManager) // Watcher watcher := server.NewConfigurationWatcher( routinesPool, providerAggregator, getDefaultsEntrypoints(staticConfiguration), "internal", ) // TLS watcher.AddListener(func(conf dynamic.Configuration) { ctx := context.Background() tlsManager.UpdateConfigs(ctx, conf.TLS.Stores, conf.TLS.Options, conf.TLS.Certificates) gauge := metricsRegistry.TLSCertsNotAfterTimestampGauge() for _, certificate := range tlsManager.GetServerCertificates() { appendCertMetric(gauge, certificate) } }) // Metrics watcher.AddListener(func(_ dynamic.Configuration) { metricsRegistry.ConfigReloadsCounter().Add(1) metricsRegistry.LastConfigReloadSuccessGauge().Set(float64(time.Now().Unix())) }) // Server Transports watcher.AddListener(func(conf dynamic.Configuration) { transportManager.Update(conf.HTTP.ServersTransports) proxyBuilder.Update(conf.HTTP.ServersTransports) dialerManager.Update(conf.TCP.ServersTransports) }) // Switch router watcher.AddListener(switchRouter(routerFactory, serverEntryPointsTCP, serverEntryPointsUDP)) // Metrics if metricsRegistry.IsEpEnabled() || metricsRegistry.IsRouterEnabled() || metricsRegistry.IsSvcEnabled() { var eps []string for key := range serverEntryPointsTCP { eps = append(eps, key) } watcher.AddListener(func(conf dynamic.Configuration) { metrics.OnConfigurationUpdate(conf, eps) }) } // TLS challenge watcher.AddListener(tlsChallengeProvider.ListenConfiguration) // Certificate Resolvers resolverNames := map[string]struct{}{} // ACME for _, p := range acmeProviders { resolverNames[p.ResolverName] = struct{}{} watcher.AddListener(p.ListenConfiguration) } // Tailscale for _, p := range tsProviders { resolverNames[p.ResolverName] = struct{}{} watcher.AddListener(p.HandleConfigUpdate) } // Certificate resolver logs watcher.AddListener(func(config dynamic.Configuration) { for rtName, rt := range config.HTTP.Routers { if rt.TLS == nil || rt.TLS.CertResolver == "" { continue } if _, ok := resolverNames[rt.TLS.CertResolver]; !ok { log.Error().Err(err).Str(logs.RouterName, rtName).Str("certificateResolver", rt.TLS.CertResolver). Msg("Router uses a nonexistent certificate resolver") } } }) return server.NewServer(routinesPool, serverEntryPointsTCP, serverEntryPointsUDP, watcher, observabilityMgr), nil } func getHTTPChallengeHandler(acmeProviders []*acme.Provider, httpChallengeProvider http.Handler) http.Handler { var acmeHTTPHandler http.Handler for _, p := range acmeProviders { if p != nil && p.HTTPChallenge != nil { acmeHTTPHandler = httpChallengeProvider break } } return acmeHTTPHandler } func getDefaultsEntrypoints(staticConfiguration *static.Configuration) []string { var defaultEntryPoints []string // Determines if at least one EntryPoint is configured to be used by default. var hasDefinedDefaults bool for _, ep := range staticConfiguration.EntryPoints { if ep.AsDefault { hasDefinedDefaults = true break } } for name, cfg := range staticConfiguration.EntryPoints { // By default all entrypoints are considered. // If at least one is flagged, then only flagged entrypoints are included. if hasDefinedDefaults && !cfg.AsDefault { continue } protocol, err := cfg.GetProtocol() if err != nil { // Should never happen because Traefik should not start if protocol is invalid. log.Error().Err(err).Msg("Invalid protocol") } if protocol != "udp" && name != static.DefaultInternalEntryPointName { defaultEntryPoints = append(defaultEntryPoints, name) } } slices.Sort(defaultEntryPoints) return defaultEntryPoints } func switchRouter(routerFactory *server.RouterFactory, serverEntryPointsTCP server.TCPEntryPoints, serverEntryPointsUDP server.UDPEntryPoints) func(conf dynamic.Configuration) { return func(conf dynamic.Configuration) { rtConf := runtime.NewConfig(conf) routers, udpRouters := routerFactory.CreateRouters(rtConf) serverEntryPointsTCP.Switch(routers) serverEntryPointsUDP.Switch(udpRouters) } } // initACMEProvider creates and registers acme.Provider instances corresponding to the configured ACME certificate resolvers. func initACMEProvider(c *static.Configuration, providerAggregator *aggregator.ProviderAggregator, tlsManager *traefiktls.Manager, httpChallengeProvider, tlsChallengeProvider challenge.Provider, routinesPool *safe.Pool) []*acme.Provider { localStores := map[string]*acme.LocalStore{} var resolvers []*acme.Provider for name, resolver := range c.CertificatesResolvers { if resolver.ACME == nil { continue } if localStores[resolver.ACME.Storage] == nil { localStores[resolver.ACME.Storage] = acme.NewLocalStore(resolver.ACME.Storage, routinesPool) } p := &acme.Provider{ Configuration: resolver.ACME, Store: localStores[resolver.ACME.Storage], ResolverName: name, HTTPChallengeProvider: httpChallengeProvider, TLSChallengeProvider: tlsChallengeProvider, } if err := providerAggregator.AddProvider(p); err != nil { log.Error().Err(err).Str("resolver", name).Msg("The ACME resolve is skipped from the resolvers list") continue } p.SetTLSManager(tlsManager) p.SetConfigListenerChan(make(chan dynamic.Configuration)) resolvers = append(resolvers, p) } return resolvers } // initTailscaleProviders creates and registers tailscale.Provider instances corresponding to the configured Tailscale certificate resolvers. func initTailscaleProviders(cfg *static.Configuration, providerAggregator *aggregator.ProviderAggregator) []*tailscale.Provider { var providers []*tailscale.Provider for name, resolver := range cfg.CertificatesResolvers { if resolver.Tailscale == nil { continue } tsProvider := &tailscale.Provider{ResolverName: name} if err := providerAggregator.AddProvider(tsProvider); err != nil { log.Error().Err(err).Str(logs.ProviderName, name).Msg("Unable to create Tailscale provider") continue } providers = append(providers, tsProvider) } return providers } func registerMetricClients(metricsConfig *types.Metrics) []metrics.Registry { if metricsConfig == nil { return nil } var registries []metrics.Registry if metricsConfig.Prometheus != nil { logger := log.With().Str(logs.MetricsProviderName, "prometheus").Logger() prometheusRegister := metrics.RegisterPrometheus(logger.WithContext(context.Background()), metricsConfig.Prometheus) if prometheusRegister != nil { registries = append(registries, prometheusRegister) logger.Debug().Msg("Configured Prometheus metrics") } } if metricsConfig.Datadog != nil { logger := log.With().Str(logs.MetricsProviderName, "datadog").Logger() registries = append(registries, metrics.RegisterDatadog(logger.WithContext(context.Background()), metricsConfig.Datadog)) logger.Debug(). Str("address", metricsConfig.Datadog.Address). Str("pushInterval", metricsConfig.Datadog.PushInterval.String()). Msgf("Configured Datadog metrics") } if metricsConfig.StatsD != nil { logger := log.With().Str(logs.MetricsProviderName, "statsd").Logger() registries = append(registries, metrics.RegisterStatsd(logger.WithContext(context.Background()), metricsConfig.StatsD)) logger.Debug(). Str("address", metricsConfig.StatsD.Address). Str("pushInterval", metricsConfig.StatsD.PushInterval.String()). Msg("Configured StatsD metrics") } if metricsConfig.InfluxDB2 != nil { logger := log.With().Str(logs.MetricsProviderName, "influxdb2").Logger() influxDB2Register := metrics.RegisterInfluxDB2(logger.WithContext(context.Background()), metricsConfig.InfluxDB2) if influxDB2Register != nil { registries = append(registries, influxDB2Register) logger.Debug(). Str("address", metricsConfig.InfluxDB2.Address). Str("bucket", metricsConfig.InfluxDB2.Bucket). Str("organization", metricsConfig.InfluxDB2.Org). Str("pushInterval", metricsConfig.InfluxDB2.PushInterval.String()). Msg("Configured InfluxDB v2 metrics") } } if metricsConfig.OTLP != nil { logger := log.With().Str(logs.MetricsProviderName, "openTelemetry").Logger() openTelemetryRegistry := metrics.RegisterOpenTelemetry(logger.WithContext(context.Background()), metricsConfig.OTLP) if openTelemetryRegistry != nil { registries = append(registries, openTelemetryRegistry) logger.Debug(). Str("pushInterval", metricsConfig.OTLP.PushInterval.String()). Msg("Configured OpenTelemetry metrics") } } return registries } func appendCertMetric(gauge gokitmetrics.Gauge, certificate *x509.Certificate) { slices.Sort(certificate.DNSNames) labels := []string{ "cn", certificate.Subject.CommonName, "serial", certificate.SerialNumber.String(), "sans", strings.Join(certificate.DNSNames, ","), } notAfter := float64(certificate.NotAfter.Unix()) gauge.With(labels...).Set(notAfter) } func setupAccessLog(conf *types.AccessLog) *accesslog.Handler { if conf == nil { return nil } accessLoggerMiddleware, err := accesslog.NewHandler(conf) if err != nil { log.Warn().Err(err).Msg("Unable to create access logger") return nil } return accessLoggerMiddleware } func setupTracing(conf *static.Tracing) (*tracing.Tracer, io.Closer) { if conf == nil { return nil, nil } tracer, closer, err := tracing.NewTracing(conf) if err != nil { log.Warn().Err(err).Msg("Unable to create tracer") return nil, nil } return tracer, closer } func checkNewVersion() { ticker := time.Tick(24 * time.Hour) safe.Go(func() { for time.Sleep(10 * time.Minute); ; <-ticker { version.CheckNewVersion() } }) } func stats(staticConfiguration *static.Configuration) { logger := log.With().Logger() if staticConfiguration.Global.SendAnonymousUsage { logger.Info().Msg(`Stats collection is enabled.`) logger.Info().Msg(`Many thanks for contributing to Traefik's improvement by allowing us to receive anonymous information from your configuration.`) logger.Info().Msg(`Help us improve Traefik by leaving this feature on :)`) logger.Info().Msg(`More details on: https://doc.traefik.io/traefik/contributing/data-collection/`) collect(staticConfiguration) } else { logger.Info().Msg(` Stats collection is disabled. Help us improve Traefik by turning this feature on :) More details on: https://doc.traefik.io/traefik/contributing/data-collection/ `) } } func collect(staticConfiguration *static.Configuration) { ticker := time.Tick(24 * time.Hour) safe.Go(func() { for time.Sleep(10 * time.Minute); ; <-ticker { if err := collector.Collect(staticConfiguration); err != nil { log.Debug().Err(err).Send() } } }) } `
return this.audioCache.get(soundName)!;
}
const audio = new Audio(`/sounds/${soundName}.mp3`);
this.audioCache.set(soundName, audio);
return audio;
}
playBetSound(): void {
const audio = this.getAudio('bet.mp3');
audio.currentTime = 0;
audio.play().catch((error) => console.error('Error playing bet sound:', error));
}
playWinSound(): void {
const audio = this.getAudio('win.mp3');
audio.currentTime = 0;
audio.play().catch((error) => console.error('Error playing win sound:', error));
}
}

View file

@ -0,0 +1,51 @@
import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class SoundInitializerService {
private renderer: Renderer2;
private observer: MutationObserver;
constructor(rendererFactory: RendererFactory2) {
this.renderer = rendererFactory.createRenderer(null, null);
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node instanceof HTMLElement) {
this.processElement(node);
}
});
});
});
}
initialize() {
document.querySelectorAll('button, a').forEach((element) => {
if (!element.hasAttribute('appPlaySound')) {
this.renderer.setAttribute(element, 'appPlaySound', '');
}
});
this.observer.observe(document.body, {
childList: true,
subtree: true,
});
}
private processElement(element: HTMLElement) {
if (
(element.tagName === 'BUTTON' || element.tagName === 'A') &&
!element.hasAttribute('appPlaySound')
) {
this.renderer.setAttribute(element, 'appPlaySound', '');
}
element.querySelectorAll('button, a').forEach((child) => {
if (!child.hasAttribute('appPlaySound')) {
this.renderer.setAttribute(child, 'appPlaySound', '');
}
});
}
}

View file

@ -174,3 +174,18 @@ a {
.modal-card .button-secondary { .modal-card .button-secondary {
@apply bg-deep-blue-light/50 hover:bg-deep-blue-light w-full py-2.5 my-2 border border-deep-blue-light/30 hover:border-deep-blue-light/50; @apply bg-deep-blue-light/50 hover:bg-deep-blue-light w-full py-2.5 my-2 border border-deep-blue-light/30 hover:border-deep-blue-light/50;
} }
button,
a {
-webkit-tap-highlight-color: transparent;
}
button[appPlaySound],
a[appPlaySound] {
cursor: pointer;
}
button:not([appPlaySound]),
a:not([appPlaySound]) {
--add-sound-directive: true;
}