Repository: ovh/cds
Pull request #5436 (2020-09-24 16:31:22)

Documents the configuration as code attribute for mutex
1. Description

Following #4565 , the documentation has to mention the configuration as code keyword to define a mutex.

1. Related issues

#4551 (closed by the implementation PR)

1. About tests

- Visual test done on a local Hugo Server

1. Mentions

@ovh/cds



Patch:
diff --git a/cli/cdsctl/admin_cdn.go b/cli/cdsctl/admin_cdn.go
index 68bf8d5ed..47a0ad03e 100644
--- a/cli/cdsctl/admin_cdn.go
+++ b/cli/cdsctl/admin_cdn.go
@@ -30,13 +30,13 @@ var adminCdnStatusCmd = cli.Command{
 }
 
 func adminCdnStatusRun(v cli.Values) (cli.ListResult, error) {
-	btes, err := client.ServiceCallGET("cdn", "/mon/status")
+	services, err := client.ServicesByType(sdk.TypeCDN)
 	if err != nil {
 		return nil, err
 	}
 	status := sdk.MonitoringStatus{}
-	if err := json.Unmarshal(btes, &status); err != nil {
-		return nil, err
+	for _, srv := range services {
+		status.Lines = append(status.Lines, srv.MonitoringStatus.Lines...)
 	}
 	return cli.AsListResult(status.Lines), nil
 }
@@ -61,7 +61,7 @@ var adminCdnCacheLogClearCmd = cli.Command{
 }
 
 func adminCdnCacheLogClearRun(v cli.Values) error {
-	return client.ServiceCallDELETE("cdn", "/cache")
+	return client.ServiceCallDELETE(sdk.TypeCDN, "/cache")
 }
 
 var adminCdnCacheLogStatusCmd = cli.Command{
@@ -71,7 +71,7 @@ var adminCdnCacheLogStatusCmd = cli.Command{
 }
 
 func adminCdnCacheLogStatusRun(v cli.Values) (cli.ListResult, error) {
-	btes, err := client.ServiceCallGET("cdn", "/cache/status")
+	btes, err := client.ServiceCallGET(sdk.TypeCDN, "/cache/status")
 	if err != nil {
 		return nil, err
 	}
@@ -104,7 +104,7 @@ var adminCdnItemSizeProjectCmd = cli.Command{
 }
 
 func adminCdnItemSizeProjectRun(v cli.Values) error {
-	btes, err := client.ServiceCallGET("cdn", "/size/item/project/"+v.GetString(_ProjectKey))
+	btes, err := client.ServiceCallGET(sdk.TypeCDN, "/size/item/project/"+v.GetString(_ProjectKey))
 	if err != nil {
 		return err
 	}
diff --git a/docs/content/development/contribute/documentation.md b/docs/content/development/contribute/documentation.md
index ba0a26812..24f3d9e2a 100644
--- a/docs/content/development/contribute/documentation.md
+++ b/docs/content/development/contribute/documentation.md
@@ -21,4 +21,4 @@ GEN_PATH=${CDS_SOURCES}/docs/content/docs/components make doc
 cd ${CDS_SOURCES}/docs
 hugo server
 ```
-* go to http://localhost:1313/cds/
+* go to http://localhost:1313/
diff --git a/docs/content/docs/concepts/files/workflow-syntax.md b/docs/content/docs/concepts/files/workflow-syntax.md
index 6d0fe7c09..e6d7f96d7 100644
--- a/docs/content/docs/concepts/files/workflow-syntax.md
+++ b/docs/content/docs/concepts/files/workflow-syntax.md
@@ -23,6 +23,7 @@ workflow:
     pipeline: deploy
     application: my-application
     environment: my-production
+    one_at_a_time: true
 hooks:
   build:
   - type: RepositoryWebHook
@@ -126,3 +127,19 @@ Example of vcs notification. Note that `pipelines` list is optional on every not
       disable_comment: false
       disable_status: false
 ```
+
+## Mutex
+
+[Mutex documentation]({{<relref "/docs/concepts/workflow/mutex.md">}})
+
+Example of a pipeline limited to one execution at a time: deployments to production cannot be executed concurently.
+
+```yml
+name: my-workflow
+workflow:
+  # ...
+  deploy:
+    pipeline: deploy
+    # ...
+    one_at_a_time: true # No concurent deployments
+```
diff --git a/docs/content/docs/concepts/workflow/mutex.md b/docs/content/docs/concepts/workflow/mutex.md
index 8be203e67..5a595ddf8 100644
--- a/docs/content/docs/concepts/workflow/mutex.md
+++ b/docs/content/docs/concepts/workflow/mutex.md
@@ -12,3 +12,7 @@ Click on the pipeline  → Edit the pipeline context → enable  "Limit one run
 ![Pipeline Mutex](/images/workflows.design.mutex.png)
 
 Examplary use case: run an integration test once on a particular environment.
+
+To configure a Mutex with the configuration as code, use the boolean property `one_at_a_time`
+in the workflow definition file, in a pipeline context section:
+[Mutex configuration as code example]({{<relref "/docs/concepts/files/workflow-syntax.md#mutex">}}).
diff --git a/engine/Makefile b/engine/Makefile
index 0e04a11d9..01a634581 100644
--- a/engine/Makefile
+++ b/engine/Makefile
@@ -26,7 +26,7 @@ TEST_DB_PORT = $(if ${CDS_DATABASE_PORT},${CDS_DATABASE_PORT},5432)
 TEST_DB_USER = $(if ${CDS_DATABASE_USER},${CDS_DATABASE_USER},cds)
 TEST_DB_PASS = $(if ${CDS_DATABASE_PASS},${CDS_DATABASE_PASS},cds)
 TEST_DB_NAME = $(if ${CDS_DATABASE_NAME},${CDS_DATABASE_NAME},cds)
-TEST_DB_DROP_DATABASE = PGPASSWORD=${TEST_DB_PASS} psql -h ${TEST_DB_HOST} -p ${TEST_DB_PORT} -U ${TEST_DB_USER} -d postgres -c "drop database ${TEST_DB_NAME}"
+TEST_DB_DROP_DATABASE = PGPASSWORD=${TEST_DB_PASS} psql -h ${TEST_DB_HOST} -p ${TEST_DB_PORT} -U ${TEST_DB_USER} -d postgres -c "drop database if exists ${TEST_DB_NAME}"
 TEST_DB_CREATE_DATABASE = PGPASSWORD=${TEST_DB_PASS} psql -h ${TEST_DB_HOST} -p ${TEST_DB_PORT} -U ${TEST_DB_USER} -d postgres -c "create database ${TEST_DB_NAME} owner ${TEST_DB_USER}"
 
 TEST_API_DB_SCHEMA = $(if ${CDS_API_DATABASE_SCHEMA},${CDS_API_DATABASE_SCHEMA},public)
@@ -121,6 +121,14 @@ test-cdn-db-create:
 	$(TEST_CDN_DB_CREATE_SCHEMA)
 	$(TEST_CDN_DB_INIT)
 
+test-db-recreate:
+	$(TEST_DB_DROP_DATABASE)
+	$(TEST_DB_CREATE_DATABASE)
+	$(TEST_API_DB_CREATE_SCHEMA)
+	$(TEST_API_DB_INIT)
+	$(TEST_CDN_DB_CREATE_SCHEMA)
+	$(TEST_CDN_DB_INIT)
+
 test-db-init: test-config
 	$(MAKE) test-api-db-create
 	$(MAKE) test-cdn-db-create
diff --git a/engine/api/admin.go b/engine/api/admin.go
index c419a6884..3ec3ca2ff 100644
--- a/engine/api/admin.go
+++ b/engine/api/admin.go
@@ -1,6 +1,7 @@
 package api
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"io/ioutil"
@@ -47,9 +48,9 @@ func (api *API) getAdminServicesHandler() service.Handler {
 
 		var err error
 		if r.FormValue("type") != "" {
-			srvs, err = services.LoadAllByType(ctx, api.mustDB(), r.FormValue("type"))
+			srvs, err = services.LoadAllByType(ctx, api.mustDB(), r.FormValue("type"), services.LoadOptions.WithStatus)
 		} else {
-			srvs, err = services.LoadAll(ctx, api.mustDB())
+			srvs, err = services.LoadAll(ctx, api.mustDB(), services.LoadOptions.WithStatus)
 		}
 		if err != nil {
 			return err
@@ -86,7 +87,7 @@ func (api *API) getAdminServiceHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
 		name := vars["name"]
-		srv, err := services.LoadByName(ctx, api.mustDB(), name)
+		srv, err := services.LoadByName(ctx, api.mustDB(), name, services.LoadOptions.WithStatus)
 		if err != nil {
 			return err
 		}
@@ -141,13 +142,14 @@ func selectDeleteAdminServiceCallHandler(api *API, method string) service.Handle
 				Message: err.Error(),
 			}, err)
 		}
+		reader := bytes.NewReader(btes)
 
 		log.Debug("selectDeleteAdminServiceCallHandler> %s : %s", query, string(btes))
 
 		if strings.HasPrefix(query, "/debug/pprof/") {
-			return service.Write(w, btes, code, "text/plain")
+			return service.Write(w, reader, code, "text/plain")
 		}
-		return service.Write(w, btes, code, "application/json")
+		return service.Write(w, reader, code, "application/json")
 	}
 }
 
@@ -173,7 +175,7 @@ func putPostAdminServiceCallHandler(api *API, method string) service.Handler {
 			}, err)
 		}
 
-		return service.Write(w, btes, code, "application/json")
+		return service.Write(w, bytes.NewReader(btes), code, "application/json")
 	}
 }
 
diff --git a/engine/api/api.go b/engine/api/api.go
index 088871b6f..159eaac25 100644
--- a/engine/api/api.go
+++ b/engine/api/api.go
@@ -630,6 +630,9 @@ func (a *API) Serve(ctx context.Context) error {
 		go event.DequeueEvent(ctx, a.mustDB())
 	}
 
+	// here the generated name of API is ready, we set ServerName with that
+	a.Common.ServiceName = event.GetCDSName()
+
 	log.Info(ctx, "Initializing internal routines...")
 	a.GoRoutines.Run(ctx, "maintenance.Subscribe", func(ctx context.Context) {
 		if err := a.listenMaintenance(ctx); err != nil {
diff --git a/engine/api/services.go b/engine/api/services.go
index 3b343dbbc..880533dab 100644
--- a/engine/api/services.go
+++ b/engine/api/services.go
@@ -99,6 +99,11 @@ func (api *API) postServiceRegisterHandler() service.Handler {
 		}
 
 		// Update or create the service
+
+		var sessionID string
+		if a := getAuthSession(ctx); a != nil {
+			sessionID = a.ID
+		}
 		if exists {
 			srv.Update(data)
 			if err := services.Update(ctx, tx, srv); err != nil {
@@ -108,12 +113,17 @@ func (api *API) postServiceRegisterHandler() service.Handler {
 		} else {
 			srv = &data
 			srv.ConsumerID = &consumer.ID
+
 			if err := services.Insert(ctx, tx, srv); err != nil {
 				return sdk.WithStack(err)
 			}
 			log.Debug("postServiceRegisterHandler> insert new service %s(%d) registered for consumer %s", srv.Name, srv.ID, *srv.ConsumerID)
 		}
 
+		if err := services.UpsertStatus(tx, *srv, sessionID); err != nil {
+			return sdk.WithStack(err)
+		}
+
 		if len(srv.PublicKey) > 0 {
 			log.Debug("postServiceRegisterHandler> service %s registered with public key: %s", srv.Name, string(srv.PublicKey))
 		}
@@ -173,10 +183,18 @@ func (api *API) postServiceHearbeatHandler() service.Handler {
 		s.LastHeartbeat = time.Now()
 		s.MonitoringStatus = mon
 
+		var sessionID string
+		if a := getAuthSession(ctx); a != nil {
+			sessionID = a.ID
+		}
 		if err := services.Update(ctx, tx, s); err != nil {
 			return err
 		}
 
+		if err := services.UpsertStatus(tx, *s, sessionID); err != nil {
+			return err
+		}
+
 		if err := tx.Commit(); err != nil {
 			return sdk.WithStack(err)
 		}
@@ -238,6 +256,10 @@ func (api *API) serviceAPIHeartbeatUpdate(ctx context.Context, db *gorp.DbMap) {
 		return
 	}
 
+	var authSessionID string
+	if a := getAuthSession(ctx); a != nil {
+		authSessionID = a.ID
+	}
 	if exists {
 		srv.ID = old.ID
 		if err := services.Update(ctx, tx, srv); err != nil {
@@ -251,6 +273,11 @@ func (api *API) serviceAPIHeartbeatUpdate(ctx context.Context, db *gorp.DbMap) {
 		}
 	}
 
+	if err := services.UpsertStatus(tx, *srv, authSessionID); err != nil {
+		log.Error(ctx, "serviceAPIHeartbeat> Unable to insert or update monitoring status %s: %v", srv.Name, err)
+		return
+	}
+
 	if err := tx.Commit(); err != nil {
 		log.Error(ctx, "serviceAPIHeartbeat> error tx commit: %v", err)
 		return
diff --git a/engine/api/services/dao.go b/engine/api/services/dao.go
index 27dbe2de8..8eafb0dad 100644
--- a/engine/api/services/dao.go
+++ b/engine/api/services/dao.go
@@ -12,15 +12,14 @@ import (
 	"github.com/ovh/cds/sdk/log"
 )
 
-func getAll(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) ([]sdk.Service, error) {
+func getAll(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query, opts ...LoadOptionFunc) ([]sdk.Service, error) {
 	ss := []service{}
-
 	if err := gorpmapping.GetAll(ctx, db, q, &ss); err != nil {
 		return nil, sdk.WrapError(err, "cannot get services")
 	}
 
 	// Check signature of data, if invalid do not return it
-	verifiedServices := make([]sdk.Service, 0, len(ss))
+	verifiedServices := make([]*sdk.Service, 0, len(ss))
 	for i := range ss {
 		isValid, err := gorpmapping.CheckSignature(ss[i], ss[i].Signature)
 		if err != nil {
@@ -30,13 +29,27 @@ func getAll(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) ([]sd
 			log.Error(ctx, "service.getAll> service %d data corrupted", ss[i].ID)
 			continue
 		}
-		verifiedServices = append(verifiedServices, ss[i].Service)
+		s := ss[i].Service
+		verifiedServices = append(verifiedServices, &s)
 	}
 
-	return verifiedServices, nil
+	if len(verifiedServices) > 0 {
+		for _, f := range opts {
+			if err := f(ctx, db, verifiedServices...); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	services := make([]sdk.Service, 0, len(verifiedServices))
+	for _, v := range verifiedServices {
+		services = append(services, *v)
+	}
+
+	return services, nil
 }
 
-func get(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) (*sdk.Service, error) {
+func get(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query, opts ...LoadOptionFunc) (*sdk.Service, error) {
 	var s service
 
 	found, err := gorpmapping.Get(ctx, db, q, &s)
@@ -56,25 +69,33 @@ func get(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) (*sdk.Se
 		return nil, sdk.WithStack(sdk.ErrNotFound)
 	}
 
+	if len(opts) > 0 {
+		for _, f := range opts {
+			if err := f(ctx, db, &s.Service); err != nil {
+				return nil, err
+			}
+		}
+	}
+
 	return &s.Service, nil
 }
 
 // LoadAll returns all services in database.
-func LoadAll(ctx context.Context, db gorp.SqlExecutor) ([]sdk.Service, error) {
+func LoadAll(ctx context.Context, db gorp.SqlExecutor, opts ...LoadOptionFunc) ([]sdk.Service, error) {
 	query := gorpmapping.NewQuery(`SELECT * FROM service`)
-	return getAll(ctx, db, query)
+	return getAll(ctx, db, query, opts...)
 }
 
 // LoadAllByType returns all services with given type.
-func LoadAllByType(ctx context.Context, db gorp.SqlExecutor, typeService string) ([]sdk.Service, error) {
+func LoadAllByType(ctx context.Context, db gorp.SqlExecutor, typeService string, opts ...LoadOptionFunc) ([]sdk.Service, error) {
 	if ss, ok := internalCache.getFromCache(typeService); ok {
 		return ss, nil
 	}
 	query := gorpmapping.NewQuery(`SELECT * FROM service WHERE type = $1`).Args(typeService)
-	return getAll(ctx, db, query)
+	return getAll(ctx, db, query, opts...)
 }
 
-// LoadAllByType returns all services that users can see with given type.
+// LoadAllByTypeAndUserID returns all services that users can see with given type.
 func LoadAllByTypeAndUserID(ctx context.Context, db gorp.SqlExecutor, typeService string, userID string) ([]sdk.Service, error) {
 	query := gorpmapping.NewQuery(`
 		SELECT service.*
@@ -97,9 +118,9 @@ func LoadByNameAndType(ctx context.Context, db gorp.SqlExecutor, name, stype str
 }
 
 // LoadByName returns a service by its name.
-func LoadByName(ctx context.Context, db gorp.SqlExecutor, name string) (*sdk.Service, error) {
+func LoadByName(ctx context.Context, db gorp.SqlExecutor, name string, opts ...LoadOptionFunc) (*sdk.Service, error) {
 	query := gorpmapping.NewQuery("SELECT * FROM service WHERE name = $1").Args(name)
-	return get(ctx, db, query)
+	return get(ctx, db, query, opts...)
 }
 
 // LoadByID returns a service by its id.
@@ -134,6 +155,28 @@ func Update(ctx context.Context, db gorpmapper.SqlExecutorWithTx, s *sdk.Service
 	return nil
 }
 
+// UpsertStatus insert or update monitoring status
+func UpsertStatus(db gorpmapper.SqlExecutorWithTx, s sdk.Service, authSessionID string) error {
+	var sessionID *string
+	if authSessionID == "" {
+		// no sessionID : we can delete service_status to keep only one status
+		// example: each api has a consumerID and no authSessionID -> so only on status per service
+		query := "delete from service_status where service_id = $1"
+		if _, err := db.Exec(query, s.ID); err != nil {
+			return sdk.WithStack(err)
+		}
+		query = `INSERT INTO service_status(monitoring_status, service_id) VALUES($1,$2)`
+		_, err := db.Exec(query, s.MonitoringStatus, s.ID)
+		return sdk.WithStack(err)
+	}
+	sessionID = &authSessionID
+	query := `INSERT INTO service_status(monitoring_status, service_id, auth_session_id) VALUES($1,$2, $3)
+	ON CONFLICT (service_id, auth_session_id) DO UPDATE SET monitoring_status = $1, service_id = $2, auth_session_id = $3`
+	_, err := db.Exec(query, s.MonitoringStatus, s.ID, sessionID)
+	return sdk.WithStack(err)
+
+}
+
 // Delete a service.
 func Delete(db gorp.SqlExecutor, s *sdk.Service) error {
 	if s.Type == sdk.TypeHatchery {
diff --git a/engine/api/services/external.go b/engine/api/services/external.go
index d8f7d644e..88c22576f 100644
--- a/engine/api/services/external.go
+++ b/engine/api/services/external.go
@@ -24,6 +24,7 @@ type ExternalService struct {
 	Path        string `json:"path"`
 }
 
+// ServiceConfig return sthe serviceConfig for the current ExternalService
 func (e ExternalService) ServiceConfig() sdk.ServiceConfig {
 	b, _ := json.Marshal(e)
 	var cfg sdk.ServiceConfig
@@ -64,7 +65,7 @@ func Pings(ctx context.Context, dbFunc func() *gorp.DbMap, ss []ExternalService)
 
 func ping(ctx context.Context, db gorpmapper.SqlExecutorWithTx, s ExternalService) error {
 	// Select for update
-	serv, err := LoadByName(context.Background(), db, s.Name)
+	srv, err := LoadByName(context.Background(), db, s.Name)
 	if err != nil {
 		return sdk.WithStack(err)
 	}
@@ -101,9 +102,13 @@ func ping(ctx context.Context, db gorpmapper.SqlExecutorWithTx, s ExternalServic
 		mon.Lines[0].Value = "Health: OK"
 	}
 
-	serv.LastHeartbeat = time.Now()
-	serv.MonitoringStatus = mon
-	if err := Update(ctx, db, serv); err != nil {
+	srv.LastHeartbeat = time.Now()
+	srv.MonitoringStatus = mon
+	if err := Update(ctx, db, srv); err != nil {
+		log.Warning(ctx, "services.ping> unable to update external service: %v", err)
+		return err
+	}
+	if err := UpsertStatus(db, *srv, ""); err != nil {
 		log.Warning(ctx, "services.ping> unable to update monitoring status: %v", err)
 		return err
 	}
@@ -155,6 +160,10 @@ func initExternal(ctx context.Context, db *gorp.DbMap, s ExternalService) error
 		}
 	}
 
+	if err := UpsertStatus(tx, s.Service, ""); err != nil {
+		return sdk.WrapError(err, "unable to insert or update monitoring status for external service")
+	}
+
 	if err := tx.Commit(); err != nil {
 		return sdk.WithStack(err)
 	}
diff --git a/engine/api/services/gorp_model.go b/engine/api/services/gorp_model.go
index bd6e8fba8..4d9ffb432 100644
--- a/engine/api/services/gorp_model.go
+++ b/engine/api/services/gorp_model.go
@@ -20,5 +20,6 @@ func (s service) Canonical() gorpmapper.CanonicalForms {
 func init() {
 	gorpmapping.Register(
 		gorpmapping.New(service{}, "service", true, "id"),
+		gorpmapping.New(sdk.ServiceStatus{}, "service_status", true, "id"),
 	)
 }
diff --git a/engine/api/services/loader.go b/engine/api/services/loader.go
new file mode 100644
index 000000000..974d4353e
--- /dev/null
+++ b/engine/api/services/loader.go
@@ -0,0 +1,66 @@
+package services
+
+import (
+	"context"
+	"time"
+
+	"github.com/go-gorp/gorp"
+	"github.com/lib/pq"
+	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/sdk"
+)
+
+// LoadOptionFunc is used as options to load services
+type LoadOptionFunc func(context.Context, gorp.SqlExecutor, ...*sdk.Service) error
+
+// LoadOptions provides all options on service loads functions
+var LoadOptions = struct {
+	WithStatus LoadOptionFunc
+}{
+	WithStatus: loadServiceStatus,
+}
+
+func loadServiceStatus(ctx context.Context, db gorp.SqlExecutor, services ...*sdk.Service) error {
+	var servicesIDs []int64
+	for _, s := range services {
+		servicesIDs = append(servicesIDs, s.ID)
+	}
+
+	ss, err := loadAllServiceStatus(ctx, db, servicesIDs)
+	if err != nil {
+		return err
+	}
+	for i := range services {
+		srv := services[i]
+		srv.MonitoringStatus = sdk.MonitoringStatus{Now: time.Now()}
+		completeStatus(ss, srv)
+	}
+
+	return nil
+}
+
+// loadAllServiceStatus returns all services status
+func loadAllServiceStatus(ctx context.Context, db gorp.SqlExecutor, servicesIDs []int64) ([]sdk.ServiceStatus, error) {
+	query := gorpmapping.NewQuery(`SELECT * FROM service_status WHERE service_id = ANY($1)`).Args(pq.Int64Array(servicesIDs))
+	ss := []sdk.ServiceStatus{}
+	if err := gorpmapping.GetAll(ctx, db, query, &ss); err != nil {
+		return nil, sdk.WrapError(err, "cannot get services")
+	}
+	return ss, nil
+}
+
+func completeStatus(ss []sdk.ServiceStatus, srv *sdk.Service) {
+	for _, status := range ss {
+		if srv.ID == status.ServiceID {
+			for _, line := range status.MonitoringStatus.Lines {
+				if status.SessionID != nil {
+					line.SessionID = *status.SessionID
+				}
+				if srv.ConsumerID != nil {
+					line.ConsumerID = *srv.ConsumerID
+				}
+				srv.MonitoringStatus.Lines = append(srv.MonitoringStatus.Lines, line)
+			}
+		}
+	}
+}
diff --git a/engine/api/status.go b/engine/api/status.go
index 1ad61c997..c74d120f6 100644
--- a/engine/api/status.go
+++ b/engine/api/status.go
@@ -35,7 +35,7 @@ func (api *API) Status(ctx context.Context) *sdk.MonitoringStatus {
 	m := api.NewMonitoringStatus()
 
 	m.AddLine(sdk.MonitoringStatusLine{Component: "Hostname", Value: event.GetHostname(), Status: sdk.MonitoringStatusOK})
-	m.AddLine(sdk.MonitoringStatusLine{Component: "CDSName", Value: event.GetCDSName(), Status: sdk.MonitoringStatusOK})
+	m.AddLine(sdk.MonitoringStatusLine{Component: "CDSName", Value: api.Name(), Status: sdk.MonitoringStatusOK})
 	m.AddLine(api.Router.StatusPanic())
 	m.AddLine(event.Status(ctx))
 	m.AddLine(api.SharedStorage.Status(ctx))
@@ -89,6 +89,7 @@ func (api *API) computeGlobalStatus(srvs []sdk.Service) sdk.MonitoringStatus {
 
 	resume := map[string]computeGlobalNumbers{
 		sdk.TypeAPI:           {},
+		sdk.TypeCDN:           {},
 		sdk.TypeRepositories:  {},
 		sdk.TypeVCS:           {},
 		sdk.TypeHooks:         {},
@@ -125,19 +126,21 @@ func (api *API) computeGlobalStatus(srvs []sdk.Service) sdk.MonitoringStatus {
 					})
 				}
 			}
-		}
 
-		t := resume[s.Type]
-		t.nbOK += nbOK
-		t.nbWarn += nbWarn
-		t.nbAlerts += nbAlert
-		t.nbSrv++
-		resume[s.Type] = t
-
-		nbg.nbOK += nbOK
-		nbg.nbWarn += nbWarn
-		nbg.nbAlerts += nbAlert
-		nbg.nbSrv++
+			if strings.Contains(l.Component, "CDSName") {
+				t := resume[s.Type]
+				t.nbOK += nbOK
+				t.nbWarn += nbWarn
+				t.nbAlerts += nbAlert
+				t.nbSrv++
+				resume[s.Type] = t
+
+				nbg.nbOK += nbOK
+				nbg.nbWarn += nbWarn
+				nbg.nbAlerts += nbAlert
+				nbg.nbSrv++
+			}
+		}
 	}
 
 	if versionOk {
diff --git a/engine/api/templates.go b/engine/api/templates.go
index 4d24d376a..6a24d9ba7 100644
--- a/engine/api/templates.go
+++ b/engine/api/templates.go
@@ -386,7 +386,7 @@ func (api *API) postTemplateApplyHandler() service.Handler {
 			if err := exportentities.TarWorkflowComponents(ctx, data, buf); err != nil {
 				return err
 			}
-			return service.Write(w, buf.Bytes(), http.StatusOK, "application/tar")
+			return service.Write(w, buf, http.StatusOK, "application/tar")
 		}
 
 		// In case we want to generated a workflow not detached from the template, we need to check if the template
@@ -470,7 +470,7 @@ func (api *API) postTemplateApplyHandler() service.Handler {
 			if err := exportentities.TarWorkflowComponents(ctx, data, buf); err != nil {
 				return err
 			}
-			return service.Write(w, buf.Bytes(), http.StatusOK, "application/tar")
+			return service.Write(w, buf, http.StatusOK, "application/tar")
 		}
 
 		msgs, wkf, oldWkf, _, err := workflow.Push(ctx, api.mustDB(), api.Cache, p, data, nil, consumer, project.DecryptWithBuiltinKey)
diff --git a/engine/api/worker.go b/engine/api/worker.go
index 294165eff..77c5ef37d 100644
--- a/engine/api/worker.go
+++ b/engine/api/worker.go
@@ -107,7 +107,7 @@ func (api *API) postRegisterWorkerHandler() service.Handler {
 		}
 
 		// Set the JWT token as a header
-		log.Debug("worker.registerWorkerHandler> X-CDS-JWT:%s", jwt[:12])
+		log.Debug("worker.registerWorkerHandler> X-CDS-JWT:%s", sdk.StringFirstN(jwt, 12))
 		w.Header().Add("X-CDS-JWT", jwt)
 
 		// Return worker info to worker itself
diff --git a/engine/api/workflow/process.go b/engine/api/workflow/process.go
index bec31069b..6d5add8e2 100644
--- a/engine/api/workflow/process.go
+++ b/engine/api/workflow/process.go
@@ -30,11 +30,7 @@ func setValuesGitInBuildParameters(run *sdk.WorkflowNodeRun, vcsInfos vcsInfos)
 
 	sdk.ParameterAddOrSetValue(&run.BuildParameters, tagGitTag, sdk.StringParameter, vcsInfos.Tag)
 	sdk.ParameterAddOrSetValue(&run.BuildParameters, tagGitHash, sdk.StringParameter, vcsInfos.Hash)
-	hashShort := run.VCSHash
-	if len(hashShort) >= 7 {
-		hashShort = hashShort[:7]
-	}
-	sdk.ParameterAddOrSetValue(&run.BuildParameters, tagGitHashShort, sdk.StringParameter, hashShort)
+	sdk.ParameterAddOrSetValue(&run.BuildParameters, tagGitHashShort, sdk.StringParameter, sdk.StringFirstN(run.VCSHash, 7))
 	sdk.ParameterAddOrSetValue(&run.BuildParameters, tagGitAuthor, sdk.StringParameter, vcsInfos.Author)
 	sdk.ParameterAddOrSetValue(&run.BuildParameters, tagGitMessage, sdk.StringParameter, vcsInfos.Message)
 	sdk.ParameterAddOrSetValue(&run.BuildParameters, tagGitURL, sdk.StringParameter, vcsInfos.URL)
diff --git a/engine/cache/cache.go b/engine/cache/cache.go
index f62dbe3f0..c5f5b925b 100644
--- a/engine/cache/cache.go
+++ b/engine/cache/cache.go
@@ -76,12 +76,18 @@ type ScoredSetStore interface {
 	ScoredSetAdd(ctx context.Context, key string, value interface{}, score float64) error
 	ScoredSetAppend(ctx context.Context, key string, value interface{}) error
 	ScoredSetScan(ctx context.Context, key string, from, to float64, dest interface{}) error
+	ScoredSetScanWithScores(ctx context.Context, key string, from, to float64) ([]SetValueWithScore, error)
 	ScoredSetRange(ctx context.Context, key string, from, to int64, dest interface{}) error
 	ScoredSetRem(ctx context.Context, key string, members ...string) error
 	SetCard(key string) (int, error)
 	HealthStore
 }
 
+type SetValueWithScore struct {
+	Score float64
+	Value json.RawMessage
+}
+
 //New init a cache
 func New(redisHost, redisPassword string, TTL int) (Store, error) {
 	return NewRedisStore(redisHost, redisPassword, TTL)
diff --git a/engine/cache/redis.go b/engine/cache/redis.go
index b44921e5a..173e1ad0f 100644
--- a/engine/cache/redis.go
+++ b/engine/cache/redis.go
@@ -628,3 +628,34 @@ func (s *RedisStore) ScoredSetScan(ctx context.Context, key string, from, to flo
 
 	return nil
 }
+
+func (s *RedisStore) ScoredSetScanWithScores(ctx context.Context, key string, from, to float64) ([]SetValueWithScore, error) {
+	min := "-inf"
+	if from != MIN {
+		min = strconv.FormatFloat(from, 'E', -1, 64)
+	}
+	max := "+inf"
+	if to != MAX {
+		max = strconv.FormatFloat(to, 'E', -1, 64)
+	}
+
+	values, err := s.Client.ZRangeByScoreWithScores(key, redis.ZRangeBy{
+		Min: min,
+		Max: max,
+	}).Result()
+	if err != nil {
+		return nil, sdk.WrapError(err, "redis zrange error")
+	}
+
+	res := make([]SetValueWithScore, len(values))
+	for i := range values {
+		res[i].Score = values[i].Score
+		s, ok := values[i].Member.(string)
+		if !ok {
+			return nil, sdk.WithStack(fmt.Errorf("set value of type %T can't be cast to json.RawMessage", values[i].Member))
+		}
+		res[i].Value = json.RawMessage(s)
+	}
+
+	return res, nil
+}
diff --git a/engine/cdn/cdn.go b/engine/cdn/cdn.go
index 92c98fa3a..2e79e4d16 100644
--- a/engine/cdn/cdn.go
+++ b/engine/cdn/cdn.go
@@ -123,7 +123,7 @@ func (s *Service) Serve(c context.Context) error {
 		storage.InitDBMapping(s.Mapper)
 
 		// Init storage units
-		s.Units, err = storage.Init(ctx, s.Mapper, s.mustDBWithCtx(ctx), s.Cfg.Units, s.GoRoutines)
+		s.Units, err = storage.Init(ctx, s.Mapper, s.mustDBWithCtx(ctx), s.GoRoutines, s.Cfg.Units)
 		if err != nil {
 			return err
 		}
diff --git a/engine/cdn/cdn_item.go b/engine/cdn/cdn_item.go
index bdcbbc9fc..27806cec4 100644
--- a/engine/cdn/cdn_item.go
+++ b/engine/cdn/cdn_item.go
@@ -9,11 +9,11 @@ import (
 	"fmt"
 	"io"
 	"math/rand"
+	"net/http"
 	"os"
 	"time"
 
 	"github.com/ovh/cds/engine/cdn/item"
-	"github.com/ovh/cds/engine/cdn/redis"
 	"github.com/ovh/cds/engine/cdn/storage"
 	"github.com/ovh/cds/engine/gorpmapper"
 	"github.com/ovh/cds/sdk"
@@ -25,47 +25,65 @@ var (
 	rnd = rand.New(rs)
 )
 
-func (s *Service) getItemLogValue(ctx context.Context, t sdk.CDNItemType, apiRefHash string, from uint, size int) (io.ReadCloser, error) {
+func (s *Service) downloadItem(ctx context.Context, t sdk.CDNItemType, apiRefHash string, w http.ResponseWriter) error {
+	if !t.IsLog() {
+		return sdk.NewErrorFrom(sdk.ErrNotImplemented, "only log item can be download for now")
+	}
+
+	rc, filename, err := s.getItemLogValue(ctx, t, apiRefHash, sdk.CDNReaderFormatText, 0, 0)
+	if err != nil {
+		return err
+	}
+	if rc == nil {
+		return sdk.WrapError(sdk.ErrNotFound, "no storage found that contains given item %s", apiRefHash)
+	}
+	w.Header().Add("Content-Type", "text/plain")
+	w.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
+
+	if _, err := io.Copy(w, rc); err != nil {
+		return sdk.WithStack(err)
+	}
+
+	return nil
+}
+
+func (s *Service) getItemLogValue(ctx context.Context, t sdk.CDNItemType, apiRefHash string, format sdk.CDNReaderFormat, from int64, size uint) (io.ReadCloser, string, error) {
 	it, err := item.LoadByAPIRefHashAndType(ctx, s.Mapper, s.mustDBWithCtx(ctx), apiRefHash, t)
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
 
+	filename := it.APIRef.ToFilename()
+
 	itemUnit, err := storage.LoadItemUnitByUnit(ctx, s.Mapper, s.mustDBWithCtx(ctx), s.Units.Buffer.ID(), it.ID)
 	if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
-		return nil, err
+		return nil, "", err
 	}
 
 	// If item is in Buffer, get from it
 	if itemUnit != nil {
-		log.Debug("Getting logs from buffer")
-		rc, err := s.Units.Buffer.NewReader(ctx, *itemUnit)
+		log.Debug("getItemLogValue> Getting logs from buffer")
+		rc, err := s.Units.Buffer.NewAdvancedReader(ctx, *itemUnit, format, from, size)
 		if err != nil {
-			return nil, err
-		}
-		redisReader, ok := rc.(*redis.Reader)
-		if ok {
-			redisReader.From = from
-			redisReader.Size = size
+			return nil, "", err
 		}
-		return redisReader, nil
+		return rc, filename, nil
 	}
 
 	// Get from cache
-	ok, _ := s.LogCache.Exist(it.ID)
-	if ok {
-		log.Debug("Getting logs from cache")
-		return s.LogCache.NewReader(it.ID, from, size), nil
+	if ok, _ := s.LogCache.Exist(it.ID); ok {
+		log.Debug("getItemLogValue> Getting logs from cache")
+		return s.LogCache.NewReader(it.ID, format, from, size), filename, nil
 	}
 
-	log.Debug("Getting logs from storage")
+	log.Debug("getItemLogValue> Getting logs from storage")
 	// Retrieve item and push it into the cache
 	if err := s.pushItemLogIntoCache(ctx, *it); err != nil {
-		return nil, err
+		return nil, "", err
 	}
 
 	// Get from cache
-	return s.LogCache.NewReader(it.ID, from, size), nil
+	return s.LogCache.NewReader(it.ID, format, from, size), filename, nil
 }
 
 func (s *Service) pushItemLogIntoCache(ctx context.Context, item sdk.CDNItem) error {
@@ -117,18 +135,24 @@ func (s *Service) pushItemLogIntoCache(ctx context.Context, item sdk.CDNItem) er
 		close(chanError)
 	}()
 	if _, err := io.Copy(cacheWriter, pr); err != nil {
+		_ = pr.Close()
+		_ = reader.Close()
+		_ = cacheWriter.Close()
 		return err
 	}
 
-	if err := reader.Close(); err != nil {
+	if err := pr.Close(); err != nil {
+		_ = reader.Close()
+		_ = cacheWriter.Close()
 		return sdk.WithStack(err)
 	}
 
-	if err := cacheWriter.Close(); err != nil {
+	if err := reader.Close(); err != nil {
+		_ = cacheWriter.Close()
 		return sdk.WithStack(err)
 	}
 
-	if err := pr.Close(); err != nil {
+	if err := cacheWriter.Close(); err != nil {
 		return sdk.WithStack(err)
 	}
 
@@ -189,6 +213,10 @@ func (s *Service) completeItem(ctx context.Context, tx gorpmapper.SqlExecutorWit
 	multiWriter := io.MultiWriter(md5Hash, sha512Hash)
 	size, err := io.Copy(multiWriter, mreader)
 	if err != nil {
+		_ = reader.Close()
+		return sdk.WithStack(err)
+	}
+	if err := reader.Close(); err != nil {
 		return sdk.WithStack(err)
 	}
 
diff --git a/engine/cdn/cdn_log.go b/engine/cdn/cdn_log.go
index 0bf57d59b..dafd5903b 100644
--- a/engine/cdn/cdn_log.go
+++ b/engine/cdn/cdn_log.go
@@ -12,6 +12,7 @@ import (
 	"time"
 
 	gocache "github.com/patrickmn/go-cache"
+	"github.com/sirupsen/logrus"
 
 	"github.com/ovh/cds/engine/cache"
 	"github.com/ovh/cds/sdk"
@@ -102,7 +103,12 @@ func (s *Service) handleConnection(ctx context.Context, conn net.Conn) {
 
 		if err := s.handleLogMessage(ctx, bytes); err != nil {
 			telemetry.Record(ctx, metricsErrors, 1)
-			log.Error(ctx, "cdn.log> %v", err)
+			isErrWithStack := sdk.IsErrorWithStack(err)
+			fields := logrus.Fields{}
+			if isErrWithStack {
+				fields["stack_trace"] = fmt.Sprintf("%+v", err)
+			}
+			log.ErrorWithFields(ctx, fields, "cdn.log> %v", err)
 			continue
 		}
 	}
diff --git a/engine/cdn/cdn_router.go b/engine/cdn/cdn_router.go
index d08a6e81e..a6fa5ea0b 100644
--- a/engine/cdn/cdn_router.go
+++ b/engine/cdn/cdn_router.go
@@ -23,8 +23,9 @@ func (s *Service) initRouter(ctx context.Context) {
 	r.Handle("/cache/status", nil, r.GET(s.getStatusCacheHandler))
 
 	r.Handle("/item/delete", nil, r.POST(s.markItemToDeleteHandler))
-	r.Handle("/item/{type}/{apiRef}", nil, r.GET(s.getItemLogsHandler))
-	r.Handle("/item/{type}/{apiRef}/download", nil, r.GET(s.getItemLogsDownloadHandler, api.Auth(false)))
+	r.Handle("/item/{type}/{apiRef}", nil, r.GET(s.getItemHandler))
+	r.Handle("/item/{type}/{apiRef}/download", nil, r.GET(s.getItemDownloadHandler, api.Auth(false)))
+	r.Handle("/item/{type}/{apiRef}/lines", nil, r.GET(s.getItemLogsLinesHandler, api.Auth(false)))
 
 	r.Handle("/sync/projects", nil, r.POST(s.syncProjectsHandler))
 
diff --git a/engine/cdn/cdn_sync.go b/engine/cdn/cdn_sync.go
index 6cf16eb66..8b55eaeac 100644
--- a/engine/cdn/cdn_sync.go
+++ b/engine/cdn/cdn_sync.go
@@ -148,6 +148,11 @@ func (s *Service) syncProjectLogs(ctx context.Context, cdsStorage *cds.CDS, pKey
 	}
 
 	for i := 0; i < len(nodeRunIds); i++ {
+		// test if node run already exists on CDN
+		if _, has := nodeRunMap[nodeRunIds[i].NodeRunID]; has {
+			results <- nil
+			continue
+		}
 		jobs <- nodeRunIds[i]
 	}
 	close(jobs)
diff --git a/engine/cdn/item_logs_handler.go b/engine/cdn/item_logs_handler.go
index ff8213636..e3c3103e6 100644
--- a/engine/cdn/item_logs_handler.go
+++ b/engine/cdn/item_logs_handler.go
@@ -2,9 +2,8 @@ package cdn
 
 import (
 	"context"
-	"fmt"
-	"io"
 	"net/http"
+	"strconv"
 	"strings"
 
 	"github.com/gorilla/mux"
@@ -47,7 +46,7 @@ func (s *Service) markItemToDeleteHandler() service.Handler {
 	}
 }
 
-func (s *Service) getItemLogsHandler() service.Handler {
+func (s *Service) getItemHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
 		itemType := sdk.CDNItemType(vars["type"])
@@ -66,43 +65,52 @@ func (s *Service) getItemLogsHandler() service.Handler {
 	}
 }
 
-func (s *Service) getItemLogsDownloadHandler() service.Handler {
+func (s *Service) getItemDownloadHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
 		itemType := sdk.CDNItemType(vars["type"])
 		if err := itemType.Validate(); err != nil {
 			return err
 		}
-		apiRef := vars["apiRef"]
-		tokenRaw := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
+		token, err := s.checkAuth(r)
+		if err != nil {
+			return err
+		}
+
+		return s.downloadItem(ctx, itemType, token.APIRefHash, w)
+	}
+}
 
-		// Check Authorization header
-		var token sdk.CDNAuthToken
-		v := authentication.NewVerifier(s.ParsedAPIPublicKey)
-		if err := v.VerifyJWS(tokenRaw, &token); err != nil {
-			return sdk.NewErrorWithStack(err, sdk.ErrUnauthorized)
+func (s *Service) getItemLogsLinesHandler() service.Handler {
+	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		vars := mux.Vars(r)
+		itemType := sdk.CDNItemType(vars["type"])
+		if !itemType.IsLog() {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid item log type")
 		}
-		if token.APIRefHash != apiRef {
-			return sdk.WithStack(sdk.ErrNotFound)
+		token, err := s.checkAuth(r)
+		if err != nil {
+			return err
 		}
 
-		rc, err := s.getItemLogValue(ctx, itemType, token.APIRefHash, 0, 0)
+		offset, err := strconv.ParseInt(r.FormValue("offset"), 10, 64)
 		if err != nil {
-			return err
+			offset = 0 // offset can be lower than 0 if we want the n last lines
+		}
+		count, err := strconv.ParseInt(r.FormValue("count"), 10, 64)
+		if err != nil || count < 0 {
+			count = 100
 		}
 
+		rc, _, err := s.getItemLogValue(ctx, itemType, token.APIRefHash, sdk.CDNReaderFormatJSON, offset, uint(count))
+		if err != nil {
+			return err
+		}
 		if rc == nil {
 			return sdk.WrapError(sdk.ErrNotFound, "no storage found that contains given item %s", token.APIRefHash)
 		}
 
-		w.Header().Add("Content-Type", "text/plain")
-		w.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.log\"", token.APIRefHash))
-
-		if _, err := io.Copy(w, rc); err != nil {
-			return sdk.WithStack(err)
-		}
-
-		return nil
+		return service.Write(w, rc, http.StatusOK, "application/json")
 	}
 }
 
@@ -120,3 +128,21 @@ func (s *Service) getSizeByProjectHandler() service.Handler {
 		return service.WriteJSON(w, size, http.StatusOK)
 	}
 }
+
+func (s *Service) checkAuth(r *http.Request) (sdk.CDNAuthToken, error) {
+	vars := mux.Vars(r)
+	apiRef := vars["apiRef"]
+	tokenRaw := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
+
+	// Check Authorization header
+	var token sdk.CDNAuthToken
+	v := authentication.NewVerifier(s.ParsedAPIPublicKey)
+	if err := v.VerifyJWS(tokenRaw, &token); err != nil {
+		return token, sdk.NewErrorWithStack(err, sdk.ErrUnauthorized)
+	}
+	if token.APIRefHash != apiRef {
+		return token, sdk.WithStack(sdk.ErrNotFound)
+	}
+
+	return token, nil
+}
diff --git a/engine/cdn/lru/redis.go b/engine/cdn/lru/redis.go
index 6364194f1..3c1c5279b 100644
--- a/engine/cdn/lru/redis.go
+++ b/engine/cdn/lru/redis.go
@@ -76,6 +76,11 @@ func (r *Redis) Len() (int, error) {
 	return r.store.SetCard(redisLruKeyCacheKey)
 }
 
+func (s *Redis) Card(itemID string) (int, error) {
+	itemKey := cache.Key(redisLruItemCacheKey, itemID)
+	return s.store.SetCard(itemKey)
+}
+
 // Size of the cache
 func (r *Redis) Size() (int64, error) {
 	lenght, err := r.Len()
@@ -122,7 +127,7 @@ func (r *Redis) NewWriter(itemID string) io.WriteCloser {
 }
 
 // NewReader instanciates a new reader
-func (r *Redis) NewReader(itemID string, from uint, s int) io.ReadCloser {
+func (r *Redis) NewReader(itemID string, format sdk.CDNReaderFormat, from int64, s uint) io.ReadCloser {
 	return &redis.Reader{
 		ReadWrite: redis.ReadWrite{
 			Store:     r.store,
@@ -130,8 +135,9 @@ func (r *Redis) NewReader(itemID string, from uint, s int) io.ReadCloser {
 			PrefixKey: redisLruItemCacheKey,
 			UsageKey:  redisLruKeyCacheKey,
 		},
-		Size: s,
-		From: from,
+		Size:   s,
+		From:   from,
+		Format: format,
 	}
 }
 
diff --git a/engine/cdn/redis/reader.go b/engine/cdn/redis/reader.go
index b8344a98a..70117caa9 100644
--- a/engine/cdn/redis/reader.go
+++ b/engine/cdn/redis/reader.go
@@ -2,29 +2,65 @@ package redis
 
 import (
 	"io"
-	"strings"
+
+	"github.com/ovh/cds/sdk"
 )
 
 type Reader struct {
 	ReadWrite
 	nextIndex     uint
-	From          uint // the offset that we want to use when reading lines from Redis
-	Size          int  // the count of lines that we want to read (0 means to the end)
+	From          int64 // the offset that we want to use when reading lines from Redis, allows negative value to get last lines
+	Size          uint  // the count of lines that we want to read (0 means to the end)
 	currentBuffer []byte
+	Format        sdk.CDNReaderFormat
+	readEOF       bool
 }
 
 func (r *Reader) loadMoreLines() error {
-	// If its first read (next index == 0), init the next index with 'from' value
-	if r.nextIndex == 0 {
-		r.nextIndex = r.From // 'from' can be 0
+	// If from is less than 0 try to substract given value to lines count
+	if r.From < 0 {
+		lineCount, err := r.card()
+		if err != nil {
+			return err
+		}
+		r.From = int64(lineCount) + r.From
+		if r.From < 0 {
+			r.From = 0
+		}
+	}
+
+	isFirstRead := r.nextIndex == 0
+	// If its first read, init the next index with 'from' value
+	if isFirstRead {
+		r.nextIndex = uint(r.From) // 'from' can be 0 but not < 0 at this point
+	}
+
+	// If first read and json format init json list, also define formatter to exec before append lines and at read end
+	if isFirstRead && r.Format == sdk.CDNReaderFormatJSON {
+		r.currentBuffer = []byte("[")
+	}
+	formatBeforeLine := func() {
+		// For json format, if not first read we should add a comma before each line object
+		if r.Format == sdk.CDNReaderFormatJSON {
+			r.currentBuffer = append(r.currentBuffer, []byte(",")...)
+		}
+	}
+	formatEnd := func() {
+		if r.Format == sdk.CDNReaderFormatJSON {
+			r.currentBuffer = append(r.currentBuffer, []byte("]")...)
+		}
 	}
 
 	// Read 100 lines if possible or only the missing lines if less than 100
-	alreadyReadLinesLength := r.nextIndex - r.From
+	alreadyReadLinesLength := r.nextIndex - uint(r.From)
 	var newNextIndex uint
 	if r.Size > 0 {
 		linesLeftToRead := uint(r.Size) - alreadyReadLinesLength
 		if linesLeftToRead == 0 {
+			if !r.readEOF {
+				r.readEOF = true
+				formatEnd()
+			}
 			return nil
 		}
 		if linesLeftToRead > 100 {
@@ -41,9 +77,21 @@ func (r *Reader) loadMoreLines() error {
 	if err != nil {
 		return err
 	}
-	if len(lines) > 0 {
-		r.currentBuffer = append(r.currentBuffer, []byte(strings.Join(lines, ""))...)
+	for i := range lines {
+		buf, err := lines[i].Format(r.Format)
+		if err != nil {
+			return err
+		}
+		if !(isFirstRead && i == 0) {
+			formatBeforeLine()
+		}
+		r.currentBuffer = append(r.currentBuffer, buf...)
+	}
+	if len(lines) == 0 && !r.readEOF {
+		r.readEOF = true
+		formatEnd()
 	}
+
 	r.nextIndex = newNextIndex
 
 	return nil
@@ -66,7 +114,7 @@ func (r *Reader) Read(p []byte) (n int, err error) {
 
 	var buffer []byte
 	if len(r.currentBuffer) > lengthToRead { // more data, return a subset of current buffer
-		buffer = r.currentBuffer[:lengthToRead-1]
+		buffer = r.currentBuffer[:lengthToRead]
 		r.currentBuffer = r.currentBuffer[lengthToRead:]
 	} else { // return all the current buffer
 		buffer = append([]byte{}, r.currentBuffer...)
diff --git a/engine/cdn/redis/readwrite.go b/engine/cdn/redis/readwrite.go
index ee6c235d0..ba601c203 100644
--- a/engine/cdn/redis/readwrite.go
+++ b/engine/cdn/redis/readwrite.go
@@ -2,12 +2,15 @@ package redis
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
 	"strconv"
 	"strings"
 	"time"
 	"unicode"
 
 	"github.com/ovh/cds/engine/cache"
+	"github.com/ovh/cds/sdk"
 )
 
 type ReadWrite struct {
@@ -17,16 +20,43 @@ type ReadWrite struct {
 	UsageKey  string
 }
 
-func (rw *ReadWrite) get(from uint, to uint) ([]string, error) {
-	var res = make([]string, to-from+1)
-	if err := rw.Store.ScoredSetScan(context.Background(), cache.Key(rw.PrefixKey, rw.ItemID), float64(from), float64(to), &res); err != nil {
-		return res, err
+type Line struct {
+	Number int64  `json:"number"`
+	Value  string `json:"value"`
+}
+
+func (l Line) Format(f sdk.CDNReaderFormat) ([]byte, error) {
+	switch f {
+	case sdk.CDNReaderFormatJSON:
+		bs, err := json.Marshal(l)
+		return bs, sdk.WithStack(err)
+	case sdk.CDNReaderFormatText:
+		return []byte(l.Value), nil
+	}
+	return nil, sdk.WithStack(fmt.Errorf("invalid given reader format '%s'", f))
+}
+
+func (rw *ReadWrite) get(from uint, to uint) ([]Line, error) {
+	res, err := rw.Store.ScoredSetScanWithScores(context.Background(), cache.Key(rw.PrefixKey, rw.ItemID), float64(from), float64(to))
+	if err != nil {
+		return nil, err
 	}
+	ls := make([]Line, len(res))
 	for i := range res {
-		res[i] = strings.TrimFunc(res[i], unicode.IsNumber)
-		res[i] = strings.TrimPrefix(res[i], "#")
+		ls[i].Number = int64(res[i].Score)
+		var value string
+		if err := json.Unmarshal(res[i].Value, &value); err != nil {
+			return nil, sdk.WrapError(err, "cannot unmarshal line value from store")
+		}
+		ls[i].Value = strings.TrimFunc(value, unicode.IsNumber)
+		ls[i].Value = strings.TrimPrefix(ls[i].Value, "#")
 	}
-	return res, nil
+	return ls, nil
+}
+
+func (rw *ReadWrite) card() (int, error) {
+	itemKey := cache.Key(rw.PrefixKey, rw.ItemID)
+	return rw.Store.SetCard(itemKey)
 }
 
 func (rw *ReadWrite) UpdateUsage() error {
diff --git a/engine/cdn/redis/writer.go b/engine/cdn/redis/writer.go
index bbb5b7958..873e2ad59 100644
--- a/engine/cdn/redis/writer.go
+++ b/engine/cdn/redis/writer.go
@@ -13,7 +13,7 @@ func (w *Writer) Write(p []byte) (int, error) {
 	}
 	var currentLine string
 	if len(lines) == 1 {
-		currentLine = lines[0]
+		currentLine = lines[0].Value
 	}
 
 	var n int
diff --git a/engine/cdn/storage/cds/cds.go b/engine/cdn/storage/cds/cds.go
index 35afeb559..b01bb69c2 100644
--- a/engine/cdn/storage/cds/cds.go
+++ b/engine/cdn/storage/cds/cds.go
@@ -7,10 +7,12 @@ import (
 	"io"
 	"io/ioutil"
 
+	"github.com/go-gorp/gorp"
 	"go.opencensus.io/stats"
 
 	"github.com/ovh/cds/engine/cdn/storage"
 	"github.com/ovh/cds/engine/cdn/storage/encryption"
+	"github.com/ovh/cds/engine/gorpmapper"
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/cdsclient"
 	"github.com/ovh/cds/sdk/telemetry"
@@ -39,7 +41,7 @@ func (c *CDS) GetClient() cdsclient.Interface {
 	return c.client
 }
 
-func (c *CDS) Init(ctx context.Context, cfg interface{}, _ *sdk.GoRoutines) error {
+func (c *CDS) Init(ctx context.Context, _ *sdk.GoRoutines, cfg interface{}) error {
 	config, is := cfg.(*storage.CDSStorageConfiguration)
 	if !is {
 		return sdk.WithStack(fmt.Errorf("invalid configuration: %T", cfg))
@@ -60,7 +62,7 @@ func (c *CDS) Init(ctx context.Context, cfg interface{}, _ *sdk.GoRoutines) erro
 	return nil
 }
 
-func (c *CDS) ItemExists(_ sdk.CDNItem) (bool, error) {
+func (c *CDS) ItemExists(ctx context.Context, m *gorpmapper.Mapper, db gorp.SqlExecutor, i sdk.CDNItem) (bool, error) {
 	return true, nil
 }
 
diff --git a/engine/cdn/storage/encryption/encryption.go b/engine/cdn/storage/encryption/encryption.go
index 47800b38a..0b07b929e 100644
--- a/engine/cdn/storage/encryption/encryption.go
+++ b/engine/cdn/storage/encryption/encryption.go
@@ -29,12 +29,12 @@ func (s *noEncryption) NewLocator(h string) (string, error) {
 
 func (s *noEncryption) Write(_ sdk.CDNItemUnit, r io.Reader, w io.Writer) error {
 	_, err := io.Copy(w, r)
-	return err
+	return sdk.WithStack(err)
 }
 
 func (*noEncryption) Read(_ sdk.CDNItemUnit, r io.Reader, w io.Writer) error {
 	_, err := io.Copy(w, r)
-	return err
+	return sdk.WithStack(err)
 }
 
 type convergentEncryption struct {
@@ -72,7 +72,7 @@ func (s *convergentEncryption) Write(i sdk.CDNItemUnit, r io.Reader, w io.Writer
 		return err
 	}
 	err = k.EncryptPipe(r, w, []byte(i.ID))
-	return sdk.WrapError(err, "[%T] unable to write item %s", s, i.ID)
+	return sdk.WrapError(err, "[%T] unable to write item %s/%s: %+v", s, i.ID, i.ItemID, i.Item.APIRef)
 }
 
 func (s *convergentEncryption) Read(i sdk.CDNItemUnit, r io.Reader, w io.Writer) error {
@@ -81,5 +81,5 @@ func (s *convergentEncryption) Read(i sdk.CDNItemUnit, r io.Reader, w io.Writer)
 		return err
 	}
 	err = k.DecryptPipe(r, w, []byte(i.ID))
-	return sdk.WrapError(err, "[%T] unable to read item %s", s, i.ItemID)
+	return sdk.WrapError(err, "[%T] unable to read item %s/%s: %+v", s, i.ID, i.ItemID, i.Item.APIRef)
 }
diff --git a/engine/cdn/storage/local/local.go b/engine/cdn/storage/local/local.go
index 64297346b..2d9a4a922 100644
--- a/engine/cdn/storage/local/local.go
+++ b/engine/cdn/storage/local/local.go
@@ -8,13 +8,15 @@ import (
 	"path/filepath"
 	"time"
 
+	"github.com/go-gorp/gorp"
+	"go.opencensus.io/stats"
+
 	"github.com/ovh/cds/engine/cdn/storage"
 	"github.com/ovh/cds/engine/cdn/storage/encryption"
+	"github.com/ovh/cds/engine/gorpmapper"
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/log"
 	"github.com/ovh/cds/sdk/telemetry"
-
-	"go.opencensus.io/stats"
 )
 
 type Local struct {
@@ -35,7 +37,7 @@ func init() {
 	storage.RegisterDriver("local", new(Local))
 }
 
-func (s *Local) Init(ctx context.Context, cfg interface{}, goRoutines *sdk.GoRoutines) error {
+func (s *Local) Init(ctx context.Context, gorts *sdk.GoRoutines, cfg interface{}) error {
 	config, is := cfg.(*storage.LocalStorageConfiguration)
 	if !is {
 		return sdk.WithStack(fmt.Errorf("invalid configuration: %T", cfg))
@@ -44,14 +46,14 @@ func (s *Local) Init(ctx context.Context, cfg interface{}, goRoutines *sdk.GoRou
 	s.ConvergentEncryption = encryption.New(config.Encryption)
 
 	if err := os.MkdirAll(s.config.Path, os.FileMode(0700)); err != nil {
-		return err
+		return sdk.WithStack(err)
 	}
 
 	if err := telemetry.InitMetricsInt64(ctx, metricsSize, metricsReaders, metricsWriters); err != nil {
 		return err
 	}
 
-	goRoutines.Run(ctx, "cdn-local-compute-size", func(ctx context.Context) {
+	gorts.Run(ctx, "cdn-local-compute-size", func(ctx context.Context) {
 		s.computeSize(ctx)
 	})
 
@@ -66,8 +68,8 @@ func (s *Local) filename(i sdk.CDNItemUnit) (string, error) {
 	return filepath.Join(s.config.Path, loc[:3], loc), nil
 }
 
-func (s *Local) ItemExists(i sdk.CDNItem) (bool, error) {
-	iu, err := s.ExistsInDatabase(i.ID)
+func (s *Local) ItemExists(ctx context.Context, m *gorpmapper.Mapper, db gorp.SqlExecutor, i sdk.CDNItem) (bool, error) {
+	iu, err := s.ExistsInDatabase(ctx, m, db, i.ID)
 	if err != nil {
 		if sdk.ErrorIs(err, sdk.ErrNotFound) {
 			return false, nil
@@ -167,7 +169,7 @@ func (s *Local) dirSize(path string) (int64, error) {
 		if !info.IsDir() {
 			size += info.Size()
 		}
-		return err
+		return sdk.WithStack(err)
 	})
 	return size, err
 }
diff --git a/engine/cdn/storage/redis/redis.go b/engine/cdn/storage/redis/redis.go
index 3e7ff9bca..db284770e 100644
--- a/engine/cdn/storage/redis/redis.go
+++ b/engine/cdn/storage/redis/redis.go
@@ -6,13 +6,15 @@ import (
 	"io"
 	"strconv"
 
+	"github.com/go-gorp/gorp"
+	"go.opencensus.io/stats"
+
 	"github.com/ovh/cds/engine/cache"
 	"github.com/ovh/cds/engine/cdn/redis"
 	"github.com/ovh/cds/engine/cdn/storage"
+	"github.com/ovh/cds/engine/gorpmapper"
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/telemetry"
-
-	"go.opencensus.io/stats"
 )
 
 var keyBuffer = cache.Key("cdn", "buffer")
@@ -32,7 +34,7 @@ func init() {
 	storage.RegisterDriver("redis", new(Redis))
 }
 
-func (s *Redis) Init(ctx context.Context, cfg interface{}, _ *sdk.GoRoutines) error {
+func (s *Redis) Init(ctx context.Context, _ *sdk.GoRoutines, cfg interface{}) error {
 	config, is := cfg.(storage.RedisBufferConfiguration)
 	if !is {
 		return sdk.WithStack(fmt.Errorf("invalid configuration: %T", cfg))
@@ -51,7 +53,7 @@ func (s *Redis) Init(ctx context.Context, cfg interface{}, _ *sdk.GoRoutines) er
 	return nil
 }
 
-func (s *Redis) ItemExists(i sdk.CDNItem) (bool, error) {
+func (s *Redis) ItemExists(ctx context.Context, m *gorpmapper.Mapper, db gorp.SqlExecutor, i sdk.CDNItem) (bool, error) {
 	size, _ := s.store.SetCard(cache.Key(keyBuffer, i.ID))
 	return size > 0, nil
 }
@@ -79,14 +81,28 @@ func (s *Redis) NewReader(_ context.Context, i sdk.CDNItemUnit) (io.ReadCloser,
 			ItemID:    i.ItemID,
 			UsageKey:  "",
 		},
-		From: 0,
-		Size: 0,
+		Format: sdk.CDNReaderFormatText,
+	}, nil
+}
+
+// NewAdvancedReader instanciate a reader from given option, format can be JSON or Text. If from is < 0, read end lines (ex: from=-100 size=0 means read the last 100 lines)
+func (s *Redis) NewAdvancedReader(_ context.Context, i sdk.CDNItemUnit, format sdk.CDNReaderFormat, from int64, size uint) (io.ReadCloser, error) {
+	return &redis.Reader{
+		ReadWrite: redis.ReadWrite{
+			Store:     s.store,
+			PrefixKey: keyBuffer,
+			ItemID:    i.ItemID,
+			UsageKey:  "",
+		},
+		From:   from,
+		Size:   size,
+		Format: format,
 	}, nil
 }
 
 func (s *Redis) Read(_ sdk.CDNItemUnit, r io.Reader, w io.Writer) error {
 	_, err := io.Copy(w, r)
-	return err
+	return sdk.WithStack(err)
 }
 
 func (s *Redis) Status(_ context.Context) []sdk.MonitoringStatusLine {
diff --git a/engine/cdn/storage/storageunit.go b/engine/cdn/storage/storageunit.go
index dc195f287..1efcdb6d4 100644
--- a/engine/cdn/storage/storageunit.go
+++ b/engine/cdn/storage/storageunit.go
@@ -45,11 +45,9 @@ var (
 type Interface interface {
 	Name() string
 	ID() string
-	Set(m *gorpmapper.Mapper, db *gorp.DbMap, u sdk.CDNUnit)
-	GorpMapper() *gorpmapper.Mapper
-	DB() *gorp.DbMap
-	Init(ctx context.Context, cfg interface{}, goRoutines *sdk.GoRoutines) error
-	ItemExists(i sdk.CDNItem) (bool, error)
+	Set(u sdk.CDNUnit)
+	Init(ctx context.Context, goRoutines *sdk.GoRoutines, cfg interface{}) error
+	ItemExists(ctx context.Context, m *gorpmapper.Mapper, db gorp.SqlExecutor, i sdk.CDNItem) (bool, error)
 	Lock()
 	Unlock()
 	Status(ctx context.Context) []sdk.MonitoringStatusLine
@@ -57,13 +55,11 @@ type Interface interface {
 
 type AbstractUnit struct {
 	sync.Mutex
-	u  sdk.CDNUnit
-	m  *gorpmapper.Mapper
-	db *gorp.DbMap
+	u sdk.CDNUnit
 }
 
-func (a *AbstractUnit) ExistsInDatabase(id string) (*sdk.CDNItemUnit, error) {
-	iu, err := LoadItemUnitByUnit(context.Background(), a.GorpMapper(), a.DB(), a.ID(), id, gorpmapper.GetOptions.WithDecryption)
+func (a *AbstractUnit) ExistsInDatabase(ctx context.Context, m *gorpmapper.Mapper, db gorp.SqlExecutor, id string) (*sdk.CDNItemUnit, error) {
+	iu, err := LoadItemUnitByUnit(ctx, m, db, a.ID(), id, gorpmapper.GetOptions.WithDecryption)
 	if err != nil {
 		return nil, err
 	}
@@ -78,19 +74,7 @@ func (a *AbstractUnit) ID() string {
 	return a.u.ID
 }
 
-func (a *AbstractUnit) DB() *gorp.DbMap {
-	return a.db
-}
-
-func (a *AbstractUnit) GorpMapper() *gorpmapper.Mapper {
-	return a.m
-}
-
-func (a *AbstractUnit) Set(m *gorpmapper.Mapper, db *gorp.DbMap, u sdk.CDNUnit) {
-	a.u = u
-	a.m = m
-	a.db = db
-}
+func (a *AbstractUnit) Set(u sdk.CDNUnit) { a.u = u }
 
 type BufferUnit interface {
 	Interface
@@ -98,6 +82,7 @@ type BufferUnit interface {
 	Append(i sdk.CDNItemUnit, value string) error
 	Card(i sdk.CDNItemUnit) (int, error)
 	NewReader(ctx context.Context, i sdk.CDNItemUnit) (io.ReadCloser, error)
+	NewAdvancedReader(ctx context.Context, i sdk.CDNItemUnit, format sdk.CDNReaderFormat, from int64, to uint) (io.ReadCloser, error)
 	Read(i sdk.CDNItemUnit, r io.Reader, w io.Writer) error
 }
 
@@ -223,7 +208,7 @@ func (r *RunningStorageUnits) Start(ctx context.Context) error {
 	return nil
 }
 
-func Init(ctx context.Context, m *gorpmapper.Mapper, db *gorp.DbMap, config Configuration, goRoutines *sdk.GoRoutines) (*RunningStorageUnits, error) {
+func Init(ctx context.Context, m *gorpmapper.Mapper, db *gorp.DbMap, gorts *sdk.GoRoutines, config Configuration) (*RunningStorageUnits, error) {
 	var result = RunningStorageUnits{
 		m:      m,
 		db:     db,
@@ -239,7 +224,7 @@ func Init(ctx context.Context, m *gorpmapper.Mapper, db *gorp.DbMap, config Conf
 	if !is {
 		return nil, fmt.Errorf("redis driver is not a buffer unit driver")
 	}
-	if err := bd.Init(ctx, config.Buffer.Redis, goRoutines); err != nil {
+	if err := bd.Init(ctx, gorts, config.Buffer.Redis); err != nil {
 		return nil, err
 	}
 	result.Buffer = bd
@@ -267,7 +252,7 @@ func Init(ctx context.Context, m *gorpmapper.Mapper, db *gorp.DbMap, config Conf
 	} else if err != nil {
 		return nil, err
 	}
-	bd.Set(m, db, *u)
+	bd.Set(*u)
 
 	if err := tx.Commit(); err != nil {
 		return nil, err
@@ -286,7 +271,7 @@ func Init(ctx context.Context, m *gorpmapper.Mapper, db *gorp.DbMap, config Conf
 			if !is {
 				return nil, sdk.WithStack(fmt.Errorf("cds driver is not a storage unit driver"))
 			}
-			if err := sd.Init(ctx, cfg.CDS, goRoutines); err != nil {
+			if err := sd.Init(ctx, gorts, cfg.CDS); err != nil {
 				return nil, err
 			}
 			storageUnit = sd
@@ -299,7 +284,7 @@ func Init(ctx context.Context, m *gorpmapper.Mapper, db *gorp.DbMap, config Conf
 			if !is {
 				return nil, sdk.WithStack(fmt.Errorf("local driver is not a storage unit driver"))
 			}
-			if err := sd.Init(ctx, cfg.Local, goRoutines); err != nil {
+			if err := sd.Init(ctx, gorts, cfg.Local); err != nil {
 				return nil, err
 			}
 			storageUnit = sd
@@ -309,7 +294,7 @@ func Init(ctx context.Context, m *gorpmapper.Mapper, db *gorp.DbMap, config Conf
 			if !is {
 				return nil, sdk.WithStack(fmt.Errorf("swift driver is not a storage unit driver"))
 			}
-			if err := sd.Init(ctx, cfg.Swift, goRoutines); err != nil {
+			if err := sd.Init(ctx, gorts, cfg.Swift); err != nil {
 				return nil, err
 			}
 			storageUnit = sd
@@ -319,7 +304,7 @@ func Init(ctx context.Context, m *gorpmapper.Mapper, db *gorp.DbMap, config Conf
 			if !is {
 				return nil, sdk.WithStack(fmt.Errorf("webdav driver is not a storage unit driver"))
 			}
-			if err := sd.Init(ctx, cfg.Webdav, goRoutines); err != nil {
+			if err := sd.Init(ctx, gorts, cfg.Webdav); err != nil {
 				return nil, err
 			}
 			storageUnit = sd
@@ -350,7 +335,7 @@ func Init(ctx context.Context, m *gorpmapper.Mapper, db *gorp.DbMap, config Conf
 		if err != nil {
 			return nil, err
 		}
-		storageUnit.Set(m, db, *u)
+		storageUnit.Set(*u)
 
 		result.Storages = append(result.Storages, storageUnit)
 		if err := tx.Commit(); err != nil {
@@ -394,13 +379,13 @@ func (s *iuSource) Name() string {
 }
 
 func (r RunningStorageUnits) GetSource(ctx context.Context, i *sdk.CDNItem) (Source, error) {
-	ok, err := r.Buffer.ItemExists(*i)
+	ok, err := r.Buffer.ItemExists(ctx, r.m, r.db, *i)
 	if err != nil {
 		return nil, err
 	}
 
 	if ok {
-		iu, err := LoadItemUnitByUnit(ctx, r.Buffer.GorpMapper(), r.Buffer.DB(), r.Buffer.ID(), i.ID, gorpmapper.GetOptions.WithDecryption)
+		iu, err := LoadItemUnitByUnit(ctx, r.m, r.db, r.Buffer.ID(), i.ID, gorpmapper.GetOptions.WithDecryption)
 		if err != nil {
 			return nil, err
 		}
@@ -415,7 +400,7 @@ func (r RunningStorageUnits) GetSource(ctx context.Context, i *sdk.CDNItem) (Sou
 
 	if len(itemUnits) == 0 {
 		log.Warning(ctx, "item %s can't be found. No unit knows it...", i.ID)
-		return nil, sdk.ErrNotFound
+		return nil, sdk.WithStack(sdk.ErrNotFound)
 	}
 
 	// Random pick a unit
diff --git a/engine/cdn/storage/storageunit_run.go b/engine/cdn/storage/storageunit_run.go
index 3046fa7bf..4acf621e0 100644
--- a/engine/cdn/storage/storageunit_run.go
+++ b/engine/cdn/storage/storageunit_run.go
@@ -17,13 +17,13 @@ import (
 func (x *RunningStorageUnits) Run(ctx context.Context, s StorageUnit) error {
 	s.Lock()
 	defer s.Unlock()
-	_, err := LoadUnitByID(ctx, s.GorpMapper(), s.DB(), s.ID())
+	_, err := LoadUnitByID(ctx, x.m, x.db, s.ID())
 	if err != nil {
 		return err
 	}
 
 	// Load items to sync
-	itemIDs, err := LoadAllItemIDUnknownByUnit(s.DB(), s.ID(), 100)
+	itemIDs, err := LoadAllItemIDUnknownByUnit(x.db, s.ID(), 100)
 	if err != nil {
 		return err
 	}
@@ -33,12 +33,12 @@ func (x *RunningStorageUnits) Run(ctx context.Context, s StorageUnit) error {
 	}
 
 	for _, id := range itemIDs {
-		tx, err := s.DB().Begin()
+		tx, err := x.db.Begin()
 		if err != nil {
 			return err
 		}
 
-		it, err := item.LoadAndLockByID(ctx, s.GorpMapper(), tx, id, gorpmapper.GetOptions.WithDecryption)
+		it, err := item.LoadAndLockByID(ctx, x.m, tx, id, gorpmapper.GetOptions.WithDecryption)
 		if err != nil {
 			if !sdk.ErrorIs(err, sdk.ErrNotFound) {
 				log.ErrorWithFields(ctx, logrus.Fields{"stack_trace": fmt.Sprintf("%+v", err)}, "%s", err)
@@ -69,7 +69,6 @@ func (x *RunningStorageUnits) runItem(ctx context.Context, tx gorpmapper.SqlExec
 	defer func() {
 		log.Debug("storage.runItem(%s, %s): %fs", dest.Name(), item.ID, time.Since(t0).Seconds())
 	}()
-	var m = dest.GorpMapper()
 
 	iu, err := x.NewItemUnit(ctx, dest, item)
 	if err != nil {
@@ -78,12 +77,12 @@ func (x *RunningStorageUnits) runItem(ctx context.Context, tx gorpmapper.SqlExec
 	iu.Item = item
 
 	// Save in database that the item is complete for the storage unit
-	if err := InsertItemUnit(ctx, m, tx, iu); err != nil {
+	if err := InsertItemUnit(ctx, x.m, tx, iu); err != nil {
 		return err
 	}
 
 	// Reload with decryption
-	iu, err = LoadItemUnitByID(ctx, m, tx, iu.ID, gorpmapper.GetOptions.WithDecryption)
+	iu, err = LoadItemUnitByID(ctx, x.m, tx, iu.ID, gorpmapper.GetOptions.WithDecryption)
 	if err != nil {
 		return err
 	}
@@ -119,17 +118,25 @@ func (x *RunningStorageUnits) runItem(ctx context.Context, tx gorpmapper.SqlExec
 	}()
 
 	if err := dest.Write(*iu, pr, writer); err != nil {
+		_ = pr.Close()
+		_ = reader.Close()
+		_ = writer.Close()
 		return err
 	}
 
 	if err := pr.Close(); err != nil {
-		return err
+		_ = reader.Close()
+		_ = writer.Close()
+		return sdk.WithStack(err)
 	}
 
 	if err := reader.Close(); err != nil {
-		return err
+		_ = writer.Close()
+		return sdk.WithStack(err)
 	}
 
+	_ = writer.Close()
+
 	for err := range chanError {
 		if err != nil {
 			return err
diff --git a/engine/cdn/storage/swift/swift.go b/engine/cdn/storage/swift/swift.go
index 18f7c47bc..eb6a06ecd 100644
--- a/engine/cdn/storage/swift/swift.go
+++ b/engine/cdn/storage/swift/swift.go
@@ -7,11 +7,13 @@ import (
 	"net/url"
 	"strings"
 
+	"github.com/go-gorp/gorp"
 	"github.com/ncw/swift"
 	"go.opencensus.io/stats"
 
 	"github.com/ovh/cds/engine/cdn/storage"
 	"github.com/ovh/cds/engine/cdn/storage/encryption"
+	"github.com/ovh/cds/engine/gorpmapper"
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/log"
 	"github.com/ovh/cds/sdk/telemetry"
@@ -37,7 +39,7 @@ func init() {
 	storage.RegisterDriver("swift", new(Swift))
 }
 
-func (s *Swift) Init(ctx context.Context, cfg interface{}, _ *sdk.GoRoutines) error {
+func (s *Swift) Init(ctx context.Context, _ *sdk.GoRoutines, cfg interface{}) error {
 	config, is := cfg.(*storage.SwiftStorageConfiguration)
 	if !is {
 		return sdk.WithStack(fmt.Errorf("invalid configuration: %T", cfg))
@@ -59,8 +61,8 @@ func (s *Swift) Init(ctx context.Context, cfg interface{}, _ *sdk.GoRoutines) er
 	return nil
 }
 
-func (s *Swift) ItemExists(i sdk.CDNItem) (bool, error) {
-	iu, err := s.ExistsInDatabase(i.ID)
+func (s *Swift) ItemExists(ctx context.Context, m *gorpmapper.Mapper, db gorp.SqlExecutor, i sdk.CDNItem) (bool, error) {
+	iu, err := s.ExistsInDatabase(ctx, m, db, i.ID)
 	if err != nil {
 		if sdk.ErrorIs(err, sdk.ErrNotFound) {
 			return false, nil
@@ -153,5 +155,4 @@ func (s *Swift) Status(ctx context.Context) []sdk.MonitoringStatusLine {
 		Value:     fmt.Sprintf("Swift OK (%d containers, %d objects, %d bytes used", info.Containers, info.Objects, info.BytesUsed),
 		Status:    sdk.MonitoringStatusOK,
 	}}
-
 }
diff --git a/engine/cdn/storage/webdav/webdav.go b/engine/cdn/storage/webdav/webdav.go
index 06d2ebd45..a309578d3 100644
--- a/engine/cdn/storage/webdav/webdav.go
+++ b/engine/cdn/storage/webdav/webdav.go
@@ -7,13 +7,16 @@ import (
 	"os"
 	"path/filepath"
 
+	"github.com/go-gorp/gorp"
+	"github.com/studio-b12/gowebdav"
+	"go.opencensus.io/stats"
+
 	"github.com/ovh/cds/engine/cdn/storage"
 	"github.com/ovh/cds/engine/cdn/storage/encryption"
+	"github.com/ovh/cds/engine/gorpmapper"
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/log"
 	"github.com/ovh/cds/sdk/telemetry"
-	"github.com/studio-b12/gowebdav"
-	"go.opencensus.io/stats"
 )
 
 type Webdav struct {
@@ -33,7 +36,7 @@ func init() {
 	storage.RegisterDriver("webdav", new(Webdav))
 }
 
-func (s *Webdav) Init(ctx context.Context, cfg interface{}, _ *sdk.GoRoutines) error {
+func (s *Webdav) Init(ctx context.Context, _ *sdk.GoRoutines, cfg interface{}) error {
 	config, is := cfg.(*storage.WebdavStorageConfiguration)
 	if !is {
 		return sdk.WithStack(fmt.Errorf("invalid configuration: %T", cfg))
@@ -60,8 +63,8 @@ func (s *Webdav) filename(i sdk.CDNItemUnit) (string, error) {
 	return filepath.Join(s.config.Path, loc[:3], loc), nil
 }
 
-func (s *Webdav) ItemExists(i sdk.CDNItem) (bool, error) {
-	iu, err := s.ExistsInDatabase(i.ID)
+func (s *Webdav) ItemExists(ctx context.Context, m *gorpmapper.Mapper, db gorp.SqlExecutor, i sdk.CDNItem) (bool, error) {
+	iu, err := s.ExistsInDatabase(ctx, m, db, i.ID)
 	if err != nil {
 		if sdk.ErrorIs(err, sdk.ErrNotFound) {
 			return false, nil
diff --git a/engine/elasticsearch/elasticsearch_router.go b/engine/elasticsearch/elasticsearch_router.go
index 8f2c228cd..0c3f598a6 100644
--- a/engine/elasticsearch/elasticsearch_router.go
+++ b/engine/elasticsearch/elasticsearch_router.go
@@ -17,7 +17,7 @@ func (s *Service) initRouter(ctx context.Context) {
 	r.SetHeaderFunc = api.DefaultHeaders
 	r.Middlewares = append(r.Middlewares, service.CheckRequestSignatureMiddleware(s.ParsedAPIPublicKey))
 	r.Handle("/mon/version", nil, r.GET(api.VersionHandler, api.Auth(false)))
-	r.Handle("/mon/status", nil, r.GET(s.getStatusHandler))
+	r.Handle("/mon/status", nil, r.GET(s.getStatusHandler, api.Auth(false)))
 	r.Handle("/mon/metrics", nil, r.GET(service.GetPrometheustMetricsHandler(s), api.Auth(false)))
 	r.Handle("/mon/metrics/all", nil, r.GET(service.GetMetricsHandler, api.Auth(false)))
 	r.Handle("/events", nil, r.GET(s.getEventsHandler), r.POST(s.postEventHandler))
diff --git a/engine/hatchery/swarm/swarm_util_create.go b/engine/hatchery/swarm/swarm_util_create.go
index 991bd6a14..d896a9805 100644
--- a/engine/hatchery/swarm/swarm_util_create.go
+++ b/engine/hatchery/swarm/swarm_util_create.go
@@ -159,7 +159,7 @@ checkImage:
 	_, next = telemetry.Span(ctx, "swarm.dockerClient.ContainerStart", telemetry.Tag(telemetry.TagWorker, cArgs.name), telemetry.Tag("network", fmt.Sprintf("%v", networkingConfig)))
 	if err := dockerClient.ContainerStart(ctx, c.ID, types.ContainerStartOptions{}); err != nil {
 		next()
-		return sdk.WrapError(err, "Unable to start container on %s: %s", dockerClient.name, c.ID[:12])
+		return sdk.WrapError(err, "Unable to start container on %s: %s", dockerClient.name, sdk.StringFirstN(c.ID, 12))
 	}
 	next()
 	return nil
diff --git a/engine/hatchery/swarm/swarm_util_kill.go b/engine/hatchery/swarm/swarm_util_kill.go
index 3df008e49..a0a0db435 100644
--- a/engine/hatchery/swarm/swarm_util_kill.go
+++ b/engine/hatchery/swarm/swarm_util_kill.go
@@ -74,7 +74,7 @@ func (h *HatcherySwarm) killAndRemove(ctx context.Context, dockerClient *dockerC
 	}
 
 	if err := h.killAndRemoveContainer(ctx, dockerClient, ID); err != nil {
-		return sdk.WrapError(err, "%s on %s", ID[:7], dockerClient.name)
+		return sdk.WrapError(err, "%s on %s", sdk.StringFirstN(ID, 7), dockerClient.name)
 	}
 
 	//If there is no network settings, stop here
@@ -89,7 +89,7 @@ func (h *HatcherySwarm) killAndRemove(ctx context.Context, dockerClient *dockerC
 		network, err := dockerClient.NetworkInspect(ctxList, cnetwork.NetworkID, types.NetworkInspectOptions{})
 		if err != nil {
 			if !strings.Contains(err.Error(), "No such network") {
-				return sdk.WrapError(err, "unable to get network for %s on %s", ID[:7], dockerClient.name)
+				return sdk.WrapError(err, "unable to get network for %s on %s", sdk.StringFirstN(ID, 7), dockerClient.name)
 			}
 			continue
 		}
@@ -130,7 +130,7 @@ func (h *HatcherySwarm) killAndRemove(ctx context.Context, dockerClient *dockerC
 				}
 
 				if err := h.killAndRemoveContainer(ctx, dockerClient, id); err != nil {
-					log.Error(ctx, "hatchery> swarm> killAndRemove> unable to kill and remove container %s on %s err:%s", id[:12], dockerClient.name, err)
+					log.Error(ctx, "hatchery> swarm> killAndRemove> unable to kill and remove container %s on %s err:%s", sdk.StringFirstN(id, 12), dockerClient.name, err)
 				}
 			}
 		}
@@ -140,7 +140,7 @@ func (h *HatcherySwarm) killAndRemove(ctx context.Context, dockerClient *dockerC
 		ctxDocker, cancelList := context.WithTimeout(context.Background(), 10*time.Second)
 		defer cancelList()
 		if err := dockerClient.NetworkRemove(ctxDocker, network.ID); err != nil {
-			log.Error(ctx, "hatchery> swarm> killAndRemove> unable to kill and remove network %s from %s err:%s", network.ID[:12], dockerClient.name, err)
+			log.Error(ctx, "hatchery> swarm> killAndRemove> unable to kill and remove network %s from %s err:%s", sdk.StringFirstN(network.ID, 12), dockerClient.name, err)
 		}
 	}
 	return nil
diff --git a/engine/hooks/bitbucket_cloud.go b/engine/hooks/bitbucket_cloud.go
index 146f9dca3..e3d2acbc2 100644
--- a/engine/hooks/bitbucket_cloud.go
+++ b/engine/hooks/bitbucket_cloud.go
@@ -68,11 +68,7 @@ func getVariableFromBitbucketCloudChange(ctx context.Context, payload map[string
 	}
 	payload[GIT_HASH_BEFORE] = change.Old.Target.Hash
 	payload[GIT_HASH] = change.New.Target.Hash
-	hashShort := change.New.Target.Hash
-	if len(hashShort) >= 7 {
-		hashShort = hashShort[:7]
-	}
-	payload[GIT_HASH_SHORT] = hashShort
+	payload[GIT_HASH_SHORT] = sdk.StringFirstN(change.New.Target.Hash, 7)
 }
 
 func getVariableFromBitbucketCloudRepository(payload map[string]interface{}, repo *BitbucketCloudRepository) {
diff --git a/engine/hooks/bitbucket_server.go b/engine/hooks/bitbucket_server.go
index f2b6394fa..37b7c7e16 100644
--- a/engine/hooks/bitbucket_server.go
+++ b/engine/hooks/bitbucket_server.go
@@ -80,11 +80,7 @@ func getVariableFromBitbucketServerChange(payload map[string]interface{}, change
 	}
 	payload[GIT_HASH_BEFORE] = change.FromHash
 	payload[GIT_HASH] = change.ToHash
-	hashShort := change.ToHash
-	if len(hashShort) >= 7 {
-		hashShort = hashShort[:7]
-	}
-	payload[GIT_HASH_SHORT] = hashShort
+	payload[GIT_HASH_SHORT] = sdk.StringFirstN(change.ToHash, 7)
 }
 
 func getVariableFromBitbucketServerRepository(payload map[string]interface{}, repo *sdk.BitbucketServerRepository) {
@@ -123,11 +119,7 @@ func getVariableFromBitbucketServerPullRequest(payload map[string]interface{}, p
 	payload[GIT_HASH] = pr.FromRef.LatestCommit
 	payload[GIT_BRANCH_DEST] = pr.ToRef.DisplayID
 	payload[GIT_HASH_DEST] = pr.ToRef.LatestCommit
-	hashShort := pr.FromRef.LatestCommit
-	if len(hashShort) >= 7 {
-		hashShort = hashShort[:7]
-	}
-	payload[GIT_HASH_SHORT] = hashShort
+	payload[GIT_HASH_SHORT] = sdk.StringFirstN(pr.FromRef.LatestCommit, 7)
 
 	getVariableFromBitbucketServerRepository(payload, &pr.ToRef.Repository)
 	getVariableFromBitbucketServerSrcRepository(payload, &pr.FromRef.Repository)
diff --git a/engine/hooks/github.go b/engine/hooks/github.go
index a57973750..9fc19fc41 100644
--- a/engine/hooks/github.go
+++ b/engine/hooks/github.go
@@ -42,11 +42,7 @@ func (s *Service) generatePayloadFromGithubRequest(ctx context.Context, t *sdk.T
 	}
 	if request.After != "" {
 		payload[GIT_HASH] = request.After
-		hashShort := request.After
-		if len(hashShort) >= 7 {
-			hashShort = hashShort[:7]
-		}
-		payload[GIT_HASH_SHORT] = hashShort
+		payload[GIT_HASH_SHORT] = sdk.StringFirstN(request.After, 7)
 	}
 
 	getPayloadFromRepository(payload, request.Repository)
diff --git a/engine/hooks/gitlab.go b/engine/hooks/gitlab.go
index 02ed29d74..5e4c963d3 100644
--- a/engine/hooks/gitlab.go
+++ b/engine/hooks/gitlab.go
@@ -50,11 +50,7 @@ func (s *Service) generatePayloadFromGitlabRequest(ctx context.Context, t *sdk.T
 	}
 	if request.After != "" {
 		payload[GIT_HASH] = request.After
-		hashShort := request.After
-		if len(hashShort) >= 7 {
-			hashShort = hashShort[:7]
-		}
-		payload[GIT_HASH_SHORT] = hashShort
+		payload[GIT_HASH_SHORT] = sdk.StringFirstN(request.After, 7)
 	}
 
 	getPayloadFromGitlabProject(payload, request.Project)
diff --git a/engine/hooks/poller.go b/engine/hooks/poller.go
index efbd8b044..8b956432f 100644
--- a/engine/hooks/poller.go
+++ b/engine/hooks/poller.go
@@ -19,11 +19,7 @@ func fillPayload(ctx context.Context, pushEvent sdk.VCSPushEvent) map[string]str
 	payload["git.author.email"] = pushEvent.Commit.Author.Email
 	payload["git.branch"] = strings.TrimPrefix(strings.TrimPrefix(pushEvent.Branch.DisplayID, "refs/heads/"), "refs/tags/")
 	payload["git.hash"] = pushEvent.Commit.Hash
-	hashShort := pushEvent.Commit.Hash
-	if len(hashShort) >= 7 {
-		hashShort = hashShort[:7]
-	}
-	payload["git.hash.short"] = hashShort
+	payload["git.hash.short"] = sdk.StringFirstN(pushEvent.Commit.Hash, 7)
 	payload["git.repository"] = pushEvent.Repo
 	payload["cds.triggered_by.username"] = pushEvent.Commit.Author.DisplayName
 	payload["cds.triggered_by.fullname"] = pushEvent.Commit.Author.Name
diff --git a/engine/hooks/scheduler.go b/engine/hooks/scheduler.go
index ae73f8f11..af6f5557c 100644
--- a/engine/hooks/scheduler.go
+++ b/engine/hooks/scheduler.go
@@ -323,7 +323,7 @@ func (s *Service) dequeueTaskExecutions(ctx context.Context) error {
 					}
 					continue
 				} else {
-					log.Error(ctx, "dequeueTaskExecutions> %s failed err[%d]: %v", t.UUID, t.NbErrors, err)
+					log.Warning(ctx, "dequeueTaskExecutions> %s failed err[%d]: %v", t.UUID, t.NbErrors, err)
 					t.LastError = err.Error()
 					t.NbErrors++
 					saveTaskExecution = true
diff --git a/engine/repositories/repositories_router.go b/engine/repositories/repositories_router.go
index 06d9709ff..511bfbaef 100644
--- a/engine/repositories/repositories_router.go
+++ b/engine/repositories/repositories_router.go
@@ -18,7 +18,7 @@ func (s *Service) initRouter(ctx context.Context) {
 	r.Middlewares = append(r.Middlewares, service.CheckRequestSignatureMiddleware(s.ParsedAPIPublicKey))
 
 	r.Handle("/mon/version", nil, r.GET(api.VersionHandler, api.Auth(false)))
-	r.Handle("/mon/status", nil, r.GET(s.getStatusHandler))
+	r.Handle("/mon/status", nil, r.GET(s.getStatusHandler, api.Auth(false)))
 	r.Handle("/mon/metrics", nil, r.GET(service.GetPrometheustMetricsHandler(s), api.Auth(false)))
 	r.Handle("/mon/metrics/all", nil, r.GET(service.GetMetricsHandler, api.Auth(false)))
 	r.Handle("/operations", nil, r.POST(s.postOperationHandler))
diff --git a/engine/service/http.go b/engine/service/http.go
index a24329608..7d90dd920 100644
--- a/engine/service/http.go
+++ b/engine/service/http.go
@@ -1,10 +1,12 @@
 package service
 
 import (
+	"bytes"
 	"context"
 	"crypto/rsa"
 	"encoding/json"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"net/http"
 	"strconv"
@@ -69,14 +71,20 @@ func Accepted(w http.ResponseWriter) error {
 }
 
 // Write is a helper function
-func Write(w http.ResponseWriter, btes []byte, status int, contentType string) error {
+func Write(w http.ResponseWriter, r io.Reader, status int, contentType string) error {
 	w.Header().Add("Content-Type", contentType)
-	w.Header().Add("Content-Length", fmt.Sprintf("%d", len(btes)))
 
 	WriteProcessTime(context.TODO(), w)
 	w.WriteHeader(status)
-	_, err := w.Write(btes)
-	return sdk.WithStack(err)
+
+	n, err := io.Copy(w, r)
+	if err != nil {
+		return sdk.WithStack(err)
+	}
+
+	w.Header().Add("Content-Length", fmt.Sprintf("%d", n))
+
+	return nil
 }
 
 // WriteJSON is a helper function to marshal json, handle errors and set Content-Type for the best
@@ -85,7 +93,7 @@ func WriteJSON(w http.ResponseWriter, data interface{}, status int) error {
 	if err != nil {
 		return sdk.WrapError(err, "Unable to marshal json data")
 	}
-	return sdk.WithStack(Write(w, b, status, "application/json"))
+	return sdk.WithStack(Write(w, bytes.NewReader(b), status, "application/json"))
 }
 
 // WriteProcessTime writes the duration of the call in the responsewriter
diff --git a/engine/service/service.go b/engine/service/service.go
index 07a91309f..0386c40db 100644
--- a/engine/service/service.go
+++ b/engine/service/service.go
@@ -3,6 +3,7 @@ package service
 import (
 	"context"
 	"fmt"
+	"os"
 	"time"
 
 	"github.com/ovh/cds/sdk"
@@ -15,10 +16,16 @@ import (
 // NewMonitoringStatus returns a MonitoringStatus for the current service
 func (c *Common) NewMonitoringStatus() *sdk.MonitoringStatus {
 	t := time.Now()
+	hostname, err := os.Hostname()
+	if err != nil {
+		log.Error(context.Background(), "NewMonitoringStatus: error on getting hostname")
+	}
+
 	s := &sdk.MonitoringStatus{
-		Now:         t,
-		ServiceType: c.Type(),
-		ServiceName: c.Name(),
+		Now:             t,
+		ServiceType:     c.Type(),
+		ServiceName:     c.Name(),
+		ServiceHostname: hostname,
 	}
 	s.AddLine(c.commonMonitoring(t)...)
 	return s
diff --git a/engine/sql/api/216_service_status.sql b/engine/sql/api/216_service_status.sql
new file mode 100644
index 000000000..b3405d219
--- /dev/null
+++ b/engine/sql/api/216_service_status.sql
@@ -0,0 +1,16 @@
+-- +migrate Up
+
+CREATE TABLE "service_status" (
+    id BIGSERIAL PRIMARY KEY,
+    auth_session_id VARCHAR(36),
+    service_id BIGSERIAL NOT NULL,
+    monitoring_status JSONB
+);
+
+SELECT create_foreign_key_idx_cascade('FK_SERVICE_STATUS_AUTH_SESSION', 'service_status', 'auth_session', 'auth_session_id', 'id');
+SELECT create_foreign_key_idx_cascade('FK_SERVICE_STATUS_SERVICE', 'service_status', 'service', 'service_id', 'id');
+SELECT create_unique_index('service_status', 'idx_service_status_uniq', 'auth_session_id,service_id');
+
+-- +migrate Down
+
+DROP table IF EXISTS "service_status";
diff --git a/engine/worker/internal/action/builtin_gitclone.go b/engine/worker/internal/action/builtin_gitclone.go
index d8f8ab7d9..fb156da27 100644
--- a/engine/worker/internal/action/builtin_gitclone.go
+++ b/engine/worker/internal/action/builtin_gitclone.go
@@ -293,18 +293,11 @@ func extractInfo(ctx context.Context, w workerruntime.Runtime, basedir, dir stri
 					Name:  "git.hash",
 					Type:  sdk.StringVariable,
 					Value: info.Hash,
-				})
-
-				hashShort := info.Hash
-				if len(hashShort) >= 7 {
-					hashShort = hashShort[:7]
-				}
-				res = append(res, sdk.Variable{
+				}, sdk.Variable{
 					Name:  "git.hash.short",
 					Type:  sdk.StringVariable,
-					Value: hashShort,
+					Value: sdk.StringFirstN(info.Hash, 7),
 				})
-
 				w.SendLog(ctx, workerruntime.LevelInfo, fmt.Sprintf("git.hash: %s", info.Hash))
 			} else {
 				w.SendLog(ctx, workerruntime.LevelInfo, "git.hash: [empty]")
diff --git a/engine/worker/internal/register.go b/engine/worker/internal/register.go
index 925f6f3ec..cfeed644c 100644
--- a/engine/worker/internal/register.go
+++ b/engine/worker/internal/register.go
@@ -11,7 +11,7 @@ import (
 // Workers need to register to main api so they can run actions
 func (w *CurrentWorker) Register(ctx context.Context) error {
 	var form sdk.WorkerRegistrationForm
-	log.Info(ctx, "Registering with Token %s on %s", w.register.token[:12], w.register.apiEndpoint)
+	log.Info(ctx, "Registering with Token %s on %s", sdk.StringFirstN(w.register.token, 12), w.register.apiEndpoint)
 
 	requirements, errR := w.client.Requirements()
 	if errR != nil {
diff --git a/go.mod b/go.mod
index 4951e275c..f7d2878da 100644
--- a/go.mod
+++ b/go.mod
@@ -92,7 +92,7 @@ require (
 	github.com/opencontainers/image-spec v1.0.1 // indirect
 	github.com/ovh/cds/sdk/interpolate v0.0.0-20190319104452-71125b036b25
 	github.com/ovh/configstore v0.3.3-0.20200701085609-a539fcf61db5
-	github.com/ovh/symmecrypt v0.4.4-0.20200921123357-53e411c7a769
+	github.com/ovh/symmecrypt v0.4.4-0.20200925135645-1cb7e4ce37a0
 	github.com/ovh/venom v0.25.0
 	github.com/patrickmn/go-cache v2.1.0+incompatible
 	github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c
@@ -158,7 +158,6 @@ require (
 	k8s.io/apimachinery v0.0.0-20190223094358-dcb391cde5ca
 	k8s.io/client-go v10.0.0+incompatible
 	k8s.io/klog v0.2.0 // indirect
-	rsc.io/quote/v3 v3.1.0 // indirect
 	sigs.k8s.io/yaml v1.1.0 // indirect
 )
 
diff --git a/go.sum b/go.sum
index 9812375e2..1009bd7cc 100644
--- a/go.sum
+++ b/go.sum
@@ -150,8 +150,6 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.1 h1:ocYkMQY5RrXTYgXl7ICpV0IXwlEQGwKIsery4gyXa1U=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -367,8 +365,8 @@ github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
 github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
 github.com/ovh/configstore v0.3.3-0.20200701085609-a539fcf61db5 h1:E5fS/QLKUEcInoJFtM3K5+6pVVTqKxwR5boVb5oZ02c=
 github.com/ovh/configstore v0.3.3-0.20200701085609-a539fcf61db5/go.mod h1:IXY5qFC5mqMIFPXZjeZiAz/qIt+mO6F6SmsOCf/yits=
-github.com/ovh/symmecrypt v0.4.4-0.20200921123357-53e411c7a769 h1:DeC3FtGFusY7qZn5hZGY6jHceM/i+IEEubZRGqjSLRU=
-github.com/ovh/symmecrypt v0.4.4-0.20200921123357-53e411c7a769/go.mod h1:566sFU1+ZRapOXcGeM4H8o+5/2l2zZcTN7bG1FvNWkE=
+github.com/ovh/symmecrypt v0.4.4-0.20200925135645-1cb7e4ce37a0 h1:rg77rC8Ln9IruWLMio9SmIJkhuGZw8Ad+KWRCieGGZk=
+github.com/ovh/symmecrypt v0.4.4-0.20200925135645-1cb7e4ce37a0/go.mod h1:566sFU1+ZRapOXcGeM4H8o+5/2l2zZcTN7bG1FvNWkE=
 github.com/ovh/venom v0.25.0 h1:q0NC8KHuG1UoWNYXqhZUM/VbOnS8ciQJr6Afn230OIw=
 github.com/ovh/venom v0.25.0/go.mod h1:vf+2wztk/hOPS9AU32/B9XhwVPxKZ97wBJLu+AGXhYQ=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -594,7 +592,6 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
 golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -706,7 +703,5 @@ k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyod
 k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c=
 k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
diff --git a/sdk/cdn.go b/sdk/cdn.go
index a963aeebb..0e32c2f59 100644
--- a/sdk/cdn.go
+++ b/sdk/cdn.go
@@ -5,6 +5,7 @@ import (
 	json "encoding/json"
 	"fmt"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/mitchellh/hashstructure"
@@ -51,6 +52,11 @@ type CDNLogAccess struct {
 	CDNURL       string `json:"cdn_url,omitempty"`
 }
 
+type CDNMarkDelete struct {
+	WorkflowID int64 `json:"workflow_id,omitempty"`
+	RunID      int64 `json:"run_id,omitempty"`
+}
+
 type CDNLogAPIRef struct {
 	ProjectKey     string `json:"project_key"`
 	WorkflowName   string `json:"workflow_name"`
@@ -70,9 +76,24 @@ type CDNLogAPIRef struct {
 	RequirementServiceName string `json:"service_name,omitempty"`
 }
 
-type CDNMarkDelete struct {
-	WorkflowID int64 `json:"workflow_id,omitempty"`
-	RunID      int64 `json:"run_id,omitempty"`
+func (a CDNLogAPIRef) ToFilename() string {
+	jobName := strings.Replace(a.NodeRunJobName, " ", "", -1)
+
+	isService := a.RequirementServiceID > 0 && a.RequirementServiceName != ""
+	var suffix string
+	if isService {
+		suffix = fmt.Sprintf("service.%s", a.RequirementServiceName)
+	} else {
+		suffix = fmt.Sprintf("step.%d", a.StepOrder)
+	}
+
+	return fmt.Sprintf("project.%s-workflow.%s-pipeline.%s-job.%s-%s.log",
+		a.ProjectKey,
+		a.WorkflowName,
+		a.NodeRunName,
+		jobName,
+		suffix,
+	)
 }
 
 func (a CDNLogAPIRef) ToHash() (string, error) {
@@ -111,9 +132,24 @@ func (t CDNItemType) Validate() error {
 	return NewErrorFrom(ErrWrongRequest, "invalid item type")
 }
 
+func (t CDNItemType) IsLog() bool {
+	switch t {
+	case CDNTypeItemStepLog, CDNTypeItemServiceLog:
+		return true
+	}
+	return false
+}
+
 const (
 	CDNTypeItemStepLog     CDNItemType = "step-log"
 	CDNTypeItemServiceLog  CDNItemType = "service-log"
 	CDNStatusItemIncoming              = "Incoming"
 	CDNStatusItemCompleted             = "Completed"
 )
+
+type CDNReaderFormat string
+
+const (
+	CDNReaderFormatJSON CDNReaderFormat = "json"
+	CDNReaderFormatText CDNReaderFormat = "text"
+)
diff --git a/sdk/cdsclient/client_worker.go b/sdk/cdsclient/client_worker.go
index dee1d0894..f40c20c86 100644
--- a/sdk/cdsclient/client_worker.go
+++ b/sdk/cdsclient/client_worker.go
@@ -77,7 +77,7 @@ func (c *client) WorkerRegister(ctx context.Context, authToken string, form sdk.
 	c.config.SessionToken = headers.Get("X-CDS-JWT")
 
 	if c.config.Verbose {
-		fmt.Printf("Registering session %s for worker %s\n", c.config.SessionToken[:12], w.Name)
+		fmt.Printf("Registering session %s for worker %s\n", sdk.StringFirstN(c.config.SessionToken, 12), w.Name)
 	}
 
 	return &w, w.Uptodate, nil
diff --git a/sdk/cdsclient/http.go b/sdk/cdsclient/http.go
index 19e6a2305..3b8935540 100644
--- a/sdk/cdsclient/http.go
+++ b/sdk/cdsclient/http.go
@@ -198,7 +198,7 @@ func (c *client) Stream(ctx context.Context, method string, path string, body io
 			return nil, nil, -1, sdk.WithStack(err)
 		}
 		if c.config.Verbose {
-			log.Println("jwt: ", resp.Token[:12])
+			log.Println("jwt: ", sdk.StringFirstN(resp.Token, 12))
 		}
 		c.config.SessionToken = resp.Token
 	}
diff --git a/sdk/hatchery/starter.go b/sdk/hatchery/starter.go
index 39b58f94e..7c3017584 100644
--- a/sdk/hatchery/starter.go
+++ b/sdk/hatchery/starter.go
@@ -256,16 +256,7 @@ func generateWorkerName(hatcheryName string, isRegister bool, modelName string)
 	if len(workerName) <= maxLength {
 		return slug.Convert(workerName)
 	}
-	if len(modelName) > 15 {
-		modelName = modelName[:15]
-	}
+	modelName = sdk.StringFirstN(modelName, 15)
 	workerName = fmt.Sprintf("%s%s%s-%s", prefix, hName, modelName, random)
-	if len(workerName) <= maxLength {
-		return slug.Convert(workerName)
-	}
-
-	if len(workerName) > maxLength {
-		return slug.Convert(workerName[:maxLength])
-	}
-	return slug.Convert(workerName)
+	return slug.Convert(sdk.StringFirstN(workerName, maxLength))
 }
diff --git a/sdk/jws/jws.go b/sdk/jws/jws.go
index cd6baf781..3ebbdd0d3 100644
--- a/sdk/jws/jws.go
+++ b/sdk/jws/jws.go
@@ -107,11 +107,14 @@ func Sign(signer jose.Signer, content interface{}) (string, error) {
 func Verify(key interface{}, s string, i interface{}) error {
 	object, err := jose.ParseSigned(s)
 	if err != nil {
+		if object != nil {
+			return sdk.WithStack(fmt.Errorf("unable to parse payload: %v signature: %v", string(object.UnsafePayloadWithoutVerification()), err))
+		}
 		return sdk.WithStack(err)
 	}
 	output, err := object.Verify(key)
 	if err != nil {
-		return sdk.WithStack(err)
+		return sdk.WithStack(fmt.Errorf("unable to verify payload: %v signature: %v", string(object.UnsafePayloadWithoutVerification()), err))
 	}
 	return sdk.WithStack(json.Unmarshal(output, i))
 }
diff --git a/sdk/services.go b/sdk/services.go
index 904f69745..290380873 100644
--- a/sdk/services.go
+++ b/sdk/services.go
@@ -34,11 +34,20 @@ type CanonicalService struct {
 type Service struct {
 	CanonicalService
 	LastHeartbeat    time.Time        `json:"last_heartbeat" db:"last_heartbeat" cli:"heartbeat"`
-	MonitoringStatus MonitoringStatus `json:"monitoring_status" db:"monitoring_status" cli:"-"`
+	MonitoringStatus MonitoringStatus `json:"monitoring_status" db:"-" cli:"-"`
 	Version          string           `json:"version" db:"-" cli:"version"`
 	Uptodate         bool             `json:"up_to_date" db:"-"`
 }
 
+// ServiceStatus contains the status for a service. The status can be attached to a sesion_id : all services except api
+// or a serviceId for API instance.
+type ServiceStatus struct {
+	ID               int64            `json:"-" db:"id"`
+	MonitoringStatus MonitoringStatus `json:"monitoring_status" db:"monitoring_status" cli:"-"`
+	ServiceID        int64            `json:"-" db:"service_id"`
+	SessionID        *string          `json:"-" db:"auth_session_id"`
+}
+
 // Update service field from new data.
 func (s *Service) Update(data Service) {
 	s.Name = data.Name
diff --git a/sdk/status.go b/sdk/status.go
index e589402f2..770e6c825 100644
--- a/sdk/status.go
+++ b/sdk/status.go
@@ -20,8 +20,9 @@ type MonitoringStatus struct {
 	Now   time.Time              `json:"now"`
 	Lines []MonitoringStatusLine `json:"lines"`
 
-	ServiceType string `json:"-"`
-	ServiceName string `json:"-"`
+	ServiceType     string `json:"-"`
+	ServiceName     string `json:"-"`
+	ServiceHostname string `json:"-"`
 }
 
 // Value returns driver.Value from workflow template request.
@@ -48,17 +49,21 @@ func (m *MonitoringStatus) AddLine(lines ...MonitoringStatusLine) {
 		l := lines[i]
 		l.Type = m.ServiceType
 		l.Service = m.ServiceName
+		l.Hostname = m.ServiceHostname
 		m.Lines = append(m.Lines, l)
 	}
 }
 
 // MonitoringStatusLine represents a CDS Component Status
 type MonitoringStatusLine struct {
-	Status    string `json:"status" cli:"status"`
-	Component string `json:"component" cli:"component"`
-	Value     string `json:"value" cli:"value"`
-	Type      string `json:"type" cli:"type"`
-	Service   string `json:"service" cli:"service"`
+	Status     string `json:"status" cli:"status"`
+	Component  string `json:"component" cli:"component"`
+	Value      string `json:"value" cli:"value"`
+	Type       string `json:"type" cli:"type"`
+	Service    string `json:"service" cli:"service"`
+	Hostname   string `json:"hostname" cli:"hostname"`
+	SessionID  string `json:"session,omitempty" cli:"session"`
+	ConsumerID string `json:"consumer,omitempty" cli:"consumer"`
 }
 
 // HTTPStatusCode returns the http status code
diff --git a/sdk/util.go b/sdk/util.go
index dd00fd393..dfc1f8cc1 100644
--- a/sdk/util.go
+++ b/sdk/util.go
@@ -55,3 +55,10 @@ func IntMapToSlice(m map[int64]struct{}) []int64 {
 	}
 	return slice
 }
+
+func StringFirstN(s string, i int) string {
+	if len(s) <= i {
+		return s
+	}
+	return s[:i]
+}
diff --git a/ui/src/app/views/admin/service/list/service.list.html b/ui/src/app/views/admin/service/list/service.list.html
index 199f557b8..5a2efada9 100644
--- a/ui/src/app/views/admin/service/list/service.list.html
+++ b/ui/src/app/views/admin/service/list/service.list.html
@@ -82,17 +82,42 @@
                         </div>
 
                         <table class="ui definition table">
+                            <thead>
+                                <th>Type</th>
+                                <th>service</th>
+                                <th>Component</th>
+                                <th>Hostname</th>
+                                <th>Status</th>
+                                <th>Value</th>
+                                <th>Consumer</th>
+                                <th>Session</th>
+                            </thead>
                             <tbody>
                                 <tr *ngFor="let line of filteredStatusLines" [ngClass]="{'error': line.status==='AL', 'warning': line.status==='WARN', 'positive': line.status==='WARN'}">
+                                    <td>
+                                        {{line.type}}
+                                    </td>
+                                    <td>
+                                        {{line.service}}
+                                    </td>
                                     <td>
                                         {{line.component}}
                                     </td>
+                                    <td>
+                                        {{line.hostname}}
+                                    </td>
                                     <td>
                                         {{line.status}}
                                     </td>
                                     <td>
                                         {{line.value}}
                                     </td>
+                                    <td>
+                                        {{line.consumer}}
+                                    </td>
+                                    <td>
+                                        {{line.session}}
+                                    </td>
                                 </tr>
                             </tbody>
                         </table>
diff --git a/ui/src/app/views/admin/service/show/service.show.html b/ui/src/app/views/admin/service/show/service.show.html
index 8abddae2a..fd608a469 100644
--- a/ui/src/app/views/admin/service/show/service.show.html
+++ b/ui/src/app/views/admin/service/show/service.show.html
@@ -40,8 +40,18 @@
                 <div class="column">
                     <h4>Status</h4>
                     <table class="ui definition table">
+                        <thead>
+                            <th>Service</th>
+                            <th>Component</th>
+                            <th>Status</th>
+                            <th>Value</th>
+                            <th>Hostname</th>
+                        </thead>
                         <tbody>
                             <tr *ngFor="let line of service.monitoring_status.lines" [ngClass]="{'error': line.status==='AL', 'warning': line.status==='WARN', 'positive': line.status==='WARN'}">
+                                <td>
+                                    {{line.service}}
+                                </td>
                                 <td>
                                     {{line.component}}
                                 </td>
@@ -51,6 +61,9 @@
                                 <td>
                                     {{line.value}}
                                 </td>
+                                <td>
+                                    {{line.hostname}}
+                                </td>
                             </tr>
                         </tbody>
                     </table>


Test patch:
diff --git a/engine/api/services/dao_test.go b/engine/api/services/dao_test.go
index d6a88f744..6c2988a2b 100644
--- a/engine/api/services/dao_test.go
+++ b/engine/api/services/dao_test.go
@@ -3,8 +3,10 @@ package services_test
 import (
 	"context"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"github.com/ovh/cds/engine/api/services"
 	"github.com/ovh/cds/engine/api/test"
@@ -55,3 +57,86 @@ func TestDAO(t *testing.T) {
 	_, err = services.FindDeadServices(context.TODO(), db, 0)
 	test.NoError(t, err)
 }
+
+func TestDAOWithStatus(t *testing.T) {
+	db, _ := test.SetupPG(t)
+
+	allSrv, err := services.LoadAll(context.TODO(), db)
+	for _, s := range allSrv {
+		if err := services.Delete(db, &s); err != nil {
+			t.Fatalf("unable to delete service: %v", err)
+		}
+	}
+
+	privateKey, err := jws.NewRandomRSAKey()
+	test.NoError(t, err)
+	publicKey, err := jws.ExportPublicKey(privateKey)
+	test.NoError(t, err)
+
+	theServiceName := sdk.RandomString(10)
+	var srv = sdk.Service{
+		CanonicalService: sdk.CanonicalService{
+			Name:      theServiceName,
+			Type:      "type-service-test",
+			PublicKey: publicKey,
+		},
+		MonitoringStatus: sdk.MonitoringStatus{
+			Now: time.Now(),
+			Lines: []sdk.MonitoringStatusLine{
+				{
+					Component: "backend/cds-backend/items",
+					Hostname:  "foofoo.local",
+					Service:   "cds-cdn",
+					Status:    "OK",
+					Type:      "cdn",
+					Value:     "90",
+				},
+			},
+		},
+	}
+
+	test.NoError(t, services.Insert(context.TODO(), db, &srv))
+	test.NoError(t, services.UpsertStatus(db, srv, ""))
+
+	srv2, err := services.LoadByName(context.TODO(), db, srv.Name)
+	test.NoError(t, err)
+
+	assert.Equal(t, srv.Name, srv2.Name)
+	assert.Equal(t, string(srv.PublicKey), string(srv2.PublicKey))
+
+	all, err := services.LoadAllByType(context.TODO(), db, srv.Type)
+	test.NoError(t, err)
+
+	assert.True(t, len(all) >= 1)
+
+	all2, err := services.LoadAll(context.TODO(), db, services.LoadOptions.WithStatus)
+	test.NoError(t, err)
+	var found bool
+	for _, s := range all2 {
+		if s.Name == theServiceName {
+			found = true
+			require.EqualValues(t, 1, len(s.MonitoringStatus.Lines))
+			for _, ss := range s.MonitoringStatus.Lines {
+				require.EqualValues(t, "backend/cds-backend/items", ss.Component)
+			}
+			break
+		}
+	}
+
+	require.True(t, found)
+
+	srv3, err := services.LoadByName(context.TODO(), db, theServiceName)
+	test.NoError(t, err)
+	require.EqualValues(t, 0, len(srv3.MonitoringStatus.Lines))
+
+	srv4, err := services.LoadByName(context.TODO(), db, theServiceName, services.LoadOptions.WithStatus)
+	test.NoError(t, err)
+	require.EqualValues(t, 1, len(srv4.MonitoringStatus.Lines))
+
+	for _, s := range all {
+		test.NoError(t, services.Delete(db, &s))
+	}
+
+	_, err = services.FindDeadServices(context.TODO(), db, 0)
+	test.NoError(t, err)
+}
diff --git a/engine/cdn/cdn_gc_test.go b/engine/cdn/cdn_gc_test.go
index 185848552..770a9e296 100644
--- a/engine/cdn/cdn_gc_test.go
+++ b/engine/cdn/cdn_gc_test.go
@@ -43,7 +43,7 @@ func TestCleanSynchronizedItem(t *testing.T) {
 	tmpDir, err := ioutil.TempDir("", t.Name()+"-cdn-1-*")
 	require.NoError(t, err)
 
-	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, storage.Configuration{
+	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
 		Buffer: storage.BufferConfiguration{
 			Name: "redis_buffer",
 			Redis: storage.RedisBufferConfiguration{
@@ -75,7 +75,7 @@ func TestCleanSynchronizedItem(t *testing.T) {
 				},
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err)
 	s.Units = cdnUnits
 
@@ -203,7 +203,7 @@ func TestCleanWaitingItem(t *testing.T) {
 		Mapper:              m,
 	}
 
-	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, storage.Configuration{
+	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
 		Buffer: storage.BufferConfiguration{
 			Name: "redis_buffer",
 			Redis: storage.RedisBufferConfiguration{
@@ -211,7 +211,7 @@ func TestCleanWaitingItem(t *testing.T) {
 				Password: cfg["redisPassword"],
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err)
 	s.Units = cdnUnits
 
diff --git a/engine/cdn/cdn_item_test.go b/engine/cdn/cdn_item_test.go
index 60caec80e..1d2e26088 100644
--- a/engine/cdn/cdn_item_test.go
+++ b/engine/cdn/cdn_item_test.go
@@ -1,7 +1,10 @@
 package cdn
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
+	"fmt"
 	"io"
 	"io/ioutil"
 	"strings"
@@ -11,6 +14,7 @@ import (
 
 	"github.com/ovh/cds/engine/cdn/item"
 	"github.com/ovh/cds/engine/cdn/lru"
+	"github.com/ovh/cds/engine/cdn/redis"
 	"github.com/ovh/cds/engine/cdn/storage"
 	cdntest "github.com/ovh/cds/engine/cdn/test"
 	"github.com/ovh/cds/engine/gorpmapper"
@@ -42,7 +46,7 @@ func TestGetItemValue(t *testing.T) {
 	tmpDir, err := ioutil.TempDir("", t.Name()+"-cdn-1-*")
 	require.NoError(t, err)
 
-	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, storage.Configuration{
+	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
 		Buffer: storage.BufferConfiguration{
 			Name: "redis_buffer",
 			Redis: storage.RedisBufferConfiguration{
@@ -59,7 +63,7 @@ func TestGetItemValue(t *testing.T) {
 				},
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err)
 	s.Units = cdnUnits
 	s.LogCache, err = lru.NewRedisLRU(db.DbMap, 1000, cfg["redisHost"], cfg["redisPassword"])
@@ -94,17 +98,17 @@ func TestGetItemValue(t *testing.T) {
 		ItemID: it.ID,
 		UnitID: s.Units.Buffer.ID(),
 	}
-	require.NoError(t, s.Units.Buffer.Add(iu, 0, "Ligne 0\n"))
-	require.NoError(t, s.Units.Buffer.Add(iu, 1, "Ligne 1\n"))
-	require.NoError(t, s.Units.Buffer.Add(iu, 2, "Ligne 2\n"))
-	require.NoError(t, s.Units.Buffer.Add(iu, 3, "Ligne 3\n"))
-	require.NoError(t, s.Units.Buffer.Add(iu, 4, "Ligne 4\n"))
-	require.NoError(t, s.Units.Buffer.Add(iu, 5, "Ligne 5\n"))
-	require.NoError(t, s.Units.Buffer.Add(iu, 6, "Ligne 6\n"))
-	require.NoError(t, s.Units.Buffer.Add(iu, 7, "Ligne 7\n"))
-	require.NoError(t, s.Units.Buffer.Add(iu, 8, "Ligne 8\n"))
-	require.NoError(t, s.Units.Buffer.Add(iu, 9, "Ligne 9\n"))
-	require.NoError(t, s.Units.Buffer.Add(iu, 10, "Ligne 10\n"))
+	require.NoError(t, s.Units.Buffer.Add(iu, 0, "Line 0\n"))
+	require.NoError(t, s.Units.Buffer.Add(iu, 1, "Line 1\n"))
+	require.NoError(t, s.Units.Buffer.Add(iu, 2, "Line 2\n"))
+	require.NoError(t, s.Units.Buffer.Add(iu, 3, "Line 3\n"))
+	require.NoError(t, s.Units.Buffer.Add(iu, 4, "Line 4\n"))
+	require.NoError(t, s.Units.Buffer.Add(iu, 5, "Line 5\n"))
+	require.NoError(t, s.Units.Buffer.Add(iu, 6, "Line 6\n"))
+	require.NoError(t, s.Units.Buffer.Add(iu, 7, "Line 7\n"))
+	require.NoError(t, s.Units.Buffer.Add(iu, 8, "Line 8\n"))
+	require.NoError(t, s.Units.Buffer.Add(iu, 9, "Line 9\n"))
+	require.NoError(t, s.Units.Buffer.Add(iu, 10, "Line 10\n"))
 
 	require.NoError(t, s.completeItem(context.TODO(), db, iu))
 	itemDB, err := item.LoadByID(context.TODO(), s.Mapper, db, it.ID, gorpmapper.GetOptions.WithDecryption)
@@ -114,14 +118,14 @@ func TestGetItemValue(t *testing.T) {
 	require.NoError(t, storage.InsertItemUnit(context.TODO(), s.Mapper, db, itemUnit))
 
 	// Get From Buffer
-	rc, err := s.getItemLogValue(context.Background(), sdk.CDNTypeItemStepLog, apiRefhash, 3, 5)
+	rc, _, err := s.getItemLogValue(context.Background(), sdk.CDNTypeItemStepLog, apiRefhash, sdk.CDNReaderFormatText, 3, 5)
 	require.NoError(t, err)
 
 	buf := new(strings.Builder)
 	_, err = io.Copy(buf, rc)
 	require.NoError(t, err)
 
-	require.Equal(t, "Ligne 3\nLigne 4\nLigne 5\nLigne 6\nLigne 7\n", buf.String())
+	require.Equal(t, "Line 3\nLine 4\nLine 5\nLine 6\nLine 7\n", buf.String())
 	n, err := s.LogCache.Len()
 	require.NoError(t, err)
 	require.Equal(t, 0, n)
@@ -135,24 +139,147 @@ func TestGetItemValue(t *testing.T) {
 	require.NoError(t, storage.DeleteItemUnit(s.Mapper, db, itemUnit))
 
 	// Get From Storage
-	rc2, err := s.getItemLogValue(context.Background(), sdk.CDNTypeItemStepLog, apiRefhash, 3, 3)
+	rc2, _, err := s.getItemLogValue(context.Background(), sdk.CDNTypeItemStepLog, apiRefhash, sdk.CDNReaderFormatText, 3, 3)
 	require.NoError(t, err)
 
 	buf2 := new(strings.Builder)
 	_, err = io.Copy(buf2, rc2)
 	require.NoError(t, err)
 
-	require.Equal(t, "Ligne 3\nLigne 4\nLigne 5\n", buf2.String())
+	require.Equal(t, "Line 3\nLine 4\nLine 5\n", buf2.String())
 	n, err = s.LogCache.Len()
 	require.NoError(t, err)
 	require.Equal(t, 1, n)
 
 	// Get all from cache
-	rc3, err := s.getItemLogValue(context.Background(), sdk.CDNTypeItemStepLog, apiRefhash, 0, 0)
+	rc3, _, err := s.getItemLogValue(context.Background(), sdk.CDNTypeItemStepLog, apiRefhash, sdk.CDNReaderFormatText, 0, 0)
 	require.NoError(t, err)
 
 	buf3 := new(strings.Builder)
 	_, err = io.Copy(buf3, rc3)
 	require.NoError(t, err)
-	require.Equal(t, "Ligne 0\nLigne 1\nLigne 2\nLigne 3\nLigne 4\nLigne 5\nLigne 6\nLigne 7\nLigne 8\nLigne 9\nLigne 10\n", buf3.String())
+	require.Equal(t, "Line 0\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10\n", buf3.String())
+
+	// Get lines from end
+	rc4, _, err := s.getItemLogValue(context.Background(), sdk.CDNTypeItemStepLog, apiRefhash, sdk.CDNReaderFormatText, -3, 2)
+	require.NoError(t, err)
+
+	buf4 := new(strings.Builder)
+	_, err = io.Copy(buf4, rc4)
+	require.NoError(t, err)
+	require.Equal(t, "Line 8\nLine 9\n", buf4.String())
+
+	// Get lines from end in JSON
+	rc5, _, err := s.getItemLogValue(context.Background(), sdk.CDNTypeItemStepLog, apiRefhash, sdk.CDNReaderFormatJSON, -3, 2)
+	require.NoError(t, err)
+
+	buf5 := new(strings.Builder)
+	_, err = io.Copy(buf5, rc5)
+	require.NoError(t, err)
+	require.Equal(t, "[{\"number\":8,\"value\":\"Line 8\\n\"},{\"number\":9,\"value\":\"Line 9\\n\"}]", buf5.String())
+}
+
+func TestGetItemValue_ThousandLines(t *testing.T) {
+	m := gorpmapper.New()
+	item.InitDBMapping(m)
+	storage.InitDBMapping(m)
+
+	log.SetLogger(t)
+	db, factory, cache, cancel := test.SetupPGToCancel(t, m, sdk.TypeCDN)
+	t.Cleanup(cancel)
+
+	cfg := test.LoadTestingConf(t, sdk.TypeCDN)
+
+	cdntest.ClearItem(t, context.TODO(), m, db)
+
+	// Create cdn service
+	s := Service{
+		DBConnectionFactory: factory,
+		Cache:               cache,
+		Mapper:              m,
+	}
+
+	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
+		Buffer: storage.BufferConfiguration{
+			Name: "redis_buffer",
+			Redis: storage.RedisBufferConfiguration{
+				Host:     cfg["redisHost"],
+				Password: cfg["redisPassword"],
+			},
+		},
+	})
+	require.NoError(t, err)
+	s.Units = cdnUnits
+	s.LogCache, err = lru.NewRedisLRU(db.DbMap, 1000, cfg["redisHost"], cfg["redisPassword"])
+	require.NoError(t, err)
+	require.NoError(t, s.LogCache.Clear())
+
+	apiRef := sdk.CDNLogAPIRef{
+		ProjectKey:     sdk.RandomString(10),
+		WorkflowName:   sdk.RandomString(10),
+		WorkflowID:     1,
+		RunID:          1,
+		NodeRunID:      1,
+		NodeRunName:    sdk.RandomString(10),
+		NodeRunJobID:   1,
+		NodeRunJobName: sdk.RandomString(10),
+		StepName:       sdk.RandomString(10),
+		StepOrder:      0,
+	}
+	apiRefhash, err := apiRef.ToHash()
+	require.NoError(t, err)
+
+	it := sdk.CDNItem{
+		ID:         sdk.UUID(),
+		APIRefHash: apiRefhash,
+		Type:       sdk.CDNTypeItemStepLog,
+		Status:     sdk.CDNStatusItemIncoming,
+		APIRef:     apiRef,
+	}
+	require.NoError(t, item.Insert(context.TODO(), s.Mapper, db, &it))
+	iu := sdk.CDNItemUnit{
+		Item:   &it,
+		ItemID: it.ID,
+		UnitID: s.Units.Buffer.ID(),
+	}
+	for i := 0; i < 1000; i++ {
+		require.NoError(t, s.Units.Buffer.Add(iu, uint(i), fmt.Sprintf("Line %d\n", i)))
+	}
+
+	require.NoError(t, s.completeItem(context.TODO(), db, iu))
+	itemDB, err := item.LoadByID(context.TODO(), s.Mapper, db, it.ID, gorpmapper.GetOptions.WithDecryption)
+	require.NoError(t, err)
+	itemUnit, err := s.Units.NewItemUnit(context.TODO(), s.Units.Buffer, itemDB)
+	require.NoError(t, err)
+	require.NoError(t, storage.InsertItemUnit(context.TODO(), s.Mapper, db, itemUnit))
+
+	// Get From Buffer
+	rc, _, err := s.getItemLogValue(context.Background(), sdk.CDNTypeItemStepLog, apiRefhash, sdk.CDNReaderFormatJSON, 773, 200)
+	require.NoError(t, err)
+
+	buf := new(bytes.Buffer)
+	_, err = io.Copy(buf, rc)
+	require.NoError(t, err)
+
+	var lines []redis.Line
+	require.NoError(t, json.Unmarshal(buf.Bytes(), &lines), "given json should be valid")
+	require.Len(t, lines, 200)
+	require.Equal(t, int64(773), lines[0].Number)
+	require.Equal(t, "Line 773\n", lines[0].Value)
+	require.Equal(t, int64(972), lines[199].Number)
+	require.Equal(t, "Line 972\n", lines[199].Value)
+
+	rc, _, err = s.getItemLogValue(context.Background(), sdk.CDNTypeItemStepLog, apiRefhash, sdk.CDNReaderFormatJSON, 773, 0)
+	require.NoError(t, err)
+
+	buf = new(bytes.Buffer)
+	_, err = io.Copy(buf, rc)
+	require.NoError(t, err)
+
+	require.NoError(t, json.Unmarshal(buf.Bytes(), &lines), "given json should be valid")
+	require.Len(t, lines, 227)
+	require.Equal(t, int64(773), lines[0].Number)
+	require.Equal(t, "Line 773\n", lines[0].Value)
+	require.Equal(t, int64(999), lines[226].Number)
+	require.Equal(t, "Line 999\n", lines[226].Value)
 }
diff --git a/engine/cdn/cdn_log_dequeue_test.go b/engine/cdn/cdn_log_dequeue_test.go
index 405784aa8..c0fe7e31e 100644
--- a/engine/cdn/cdn_log_dequeue_test.go
+++ b/engine/cdn/cdn_log_dequeue_test.go
@@ -45,7 +45,7 @@ func TestStoreNewStepLog(t *testing.T) {
 		Mapper:              m,
 	}
 
-	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, storage.Configuration{
+	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
 		Buffer: storage.BufferConfiguration{
 			Name: "redis_buffer",
 			Redis: storage.RedisBufferConfiguration{
@@ -53,7 +53,7 @@ func TestStoreNewStepLog(t *testing.T) {
 				Password: cfg["redisPassword"],
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err)
 	s.Units = cdnUnits
 
@@ -139,7 +139,7 @@ func TestStoreLastStepLog(t *testing.T) {
 		Mapper:              m,
 	}
 
-	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, storage.Configuration{
+	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
 		Buffer: storage.BufferConfiguration{
 			Name: "redis_buffer",
 			Redis: storage.RedisBufferConfiguration{
@@ -147,7 +147,7 @@ func TestStoreLastStepLog(t *testing.T) {
 				Password: cfg["redisPassword"],
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err)
 	s.Units = cdnUnits
 
@@ -237,7 +237,7 @@ func TestStoreLogWrongOrder(t *testing.T) {
 		Mapper:              m,
 	}
 
-	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, storage.Configuration{
+	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
 		Buffer: storage.BufferConfiguration{
 			Name: "redis_buffer",
 			Redis: storage.RedisBufferConfiguration{
@@ -245,7 +245,7 @@ func TestStoreLogWrongOrder(t *testing.T) {
 				Password: cfg["redisPassword"],
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err)
 	s.Units = cdnUnits
 
@@ -366,7 +366,7 @@ func TestStoreNewServiceLogAndAppend(t *testing.T) {
 		Mapper:              m,
 	}
 
-	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, storage.Configuration{
+	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
 		Buffer: storage.BufferConfiguration{
 			Name: "redis_buffer",
 			Redis: storage.RedisBufferConfiguration{
@@ -374,7 +374,7 @@ func TestStoreNewServiceLogAndAppend(t *testing.T) {
 				Password: cfg["redisPassword"],
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err)
 	s.Units = cdnUnits
 
diff --git a/engine/cdn/cdn_sync_test.go b/engine/cdn/cdn_sync_test.go
index 35f8ef942..42629fcad 100644
--- a/engine/cdn/cdn_sync_test.go
+++ b/engine/cdn/cdn_sync_test.go
@@ -46,7 +46,7 @@ func TestSyncLog(t *testing.T) {
 		Token: "mytoken",
 	}
 
-	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, storage.Configuration{
+	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
 		Buffer: storage.BufferConfiguration{
 			Name: "redis_buffer",
 			Redis: storage.RedisBufferConfiguration{
@@ -61,7 +61,7 @@ func TestSyncLog(t *testing.T) {
 				CDS:  cdsConfig,
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err)
 	require.NoError(t, cdnUnits.Start(context.TODO()))
 	s.Units = cdnUnits
diff --git a/engine/cdn/item_logs_handler_test.go b/engine/cdn/item_logs_handler_test.go
index 4a3fbbd77..168f65dd0 100644
--- a/engine/cdn/item_logs_handler_test.go
+++ b/engine/cdn/item_logs_handler_test.go
@@ -2,6 +2,7 @@ package cdn
 
 import (
 	"context"
+	"encoding/json"
 	"net/http/httptest"
 	"strconv"
 	"testing"
@@ -14,6 +15,7 @@ import (
 	"github.com/ovh/cds/engine/api/test/assets"
 	"github.com/ovh/cds/engine/authentication"
 	"github.com/ovh/cds/engine/cdn/item"
+	"github.com/ovh/cds/engine/cdn/redis"
 	"github.com/ovh/cds/engine/cdn/storage"
 	cdntest "github.com/ovh/cds/engine/cdn/test"
 	"github.com/ovh/cds/engine/test"
@@ -107,7 +109,7 @@ func TestGetItemLogsDownloadHandler(t *testing.T) {
 
 	// Create cdn service with need storage and test item
 	s, db := newTestService(t)
-	cdnUnits, err := storage.Init(context.TODO(), s.Mapper, db.DbMap, storage.Configuration{
+	cdnUnits, err := storage.Init(context.TODO(), s.Mapper, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
 		Buffer: storage.BufferConfiguration{
 			Name: "redis_buffer",
 			Redis: storage.RedisBufferConfiguration{
@@ -115,7 +117,7 @@ func TestGetItemLogsDownloadHandler(t *testing.T) {
 				Password: cfg["redisPassword"],
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err)
 	s.Units = cdnUnits
 
@@ -167,7 +169,7 @@ func TestGetItemLogsDownloadHandler(t *testing.T) {
 	tokenRaw, err := signer.SignJWS(sdk.CDNAuthToken{APIRefHash: apiRefHash}, time.Minute)
 	require.NoError(t, err)
 
-	uri := s.Router.GetRoute("GET", s.getItemLogsDownloadHandler, map[string]string{
+	uri := s.Router.GetRoute("GET", s.getItemDownloadHandler, map[string]string{
 		"type":   string(sdk.CDNTypeItemStepLog),
 		"apiRef": apiRefHash,
 	})
@@ -179,3 +181,85 @@ func TestGetItemLogsDownloadHandler(t *testing.T) {
 
 	assert.Equal(t, "[EMERGENCY] this is a message\n", string(rec.Body.Bytes()))
 }
+
+func TestGetItemLogsLinesHandler(t *testing.T) {
+	cfg := test.LoadTestingConf(t, sdk.TypeCDN)
+
+	// Create cdn service with need storage and test item
+	s, db := newTestService(t)
+	cdnUnits, err := storage.Init(context.TODO(), s.Mapper, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
+		Buffer: storage.BufferConfiguration{
+			Name: "redis_buffer",
+			Redis: storage.RedisBufferConfiguration{
+				Host:     cfg["redisHost"],
+				Password: cfg["redisPassword"],
+			},
+		},
+	})
+	require.NoError(t, err)
+	s.Units = cdnUnits
+
+	hm := handledMessage{
+		Msg: hook.Message{
+			Full: "this is a message",
+		},
+		Status: sdk.StatusSuccess,
+		Line:   2,
+		Signature: log.Signature{
+			ProjectKey:   sdk.RandomString(10),
+			WorkflowID:   1,
+			WorkflowName: "MyWorklow",
+			RunID:        1,
+			NodeRunID:    1,
+			NodeRunName:  "MyPipeline",
+			JobName:      "MyJob",
+			JobID:        1,
+			Worker: &log.SignatureWorker{
+				StepName:  "script1",
+				StepOrder: 1,
+			},
+		},
+	}
+
+	content := buildMessage(hm)
+	err = s.storeLogs(context.TODO(), sdk.CDNTypeItemStepLog, hm.Signature, hm.Status, content, hm.Line)
+	require.NoError(t, err)
+
+	signer, err := authentication.NewSigner("cdn-test", test.SigningKey)
+	require.NoError(t, err)
+	s.Common.ParsedAPIPublicKey = signer.GetVerifyKey()
+
+	apiRef := sdk.CDNLogAPIRef{
+		ProjectKey:     hm.Signature.ProjectKey,
+		WorkflowName:   hm.Signature.WorkflowName,
+		WorkflowID:     hm.Signature.WorkflowID,
+		RunID:          hm.Signature.RunID,
+		NodeRunName:    hm.Signature.NodeRunName,
+		NodeRunID:      hm.Signature.NodeRunID,
+		NodeRunJobName: hm.Signature.JobName,
+		NodeRunJobID:   hm.Signature.JobID,
+		StepName:       hm.Signature.Worker.StepName,
+		StepOrder:      hm.Signature.Worker.StepOrder,
+	}
+	apiRefHashU, err := hashstructure.Hash(apiRef, nil)
+	require.NoError(t, err)
+	apiRefHash := strconv.FormatUint(apiRefHashU, 10)
+	tokenRaw, err := signer.SignJWS(sdk.CDNAuthToken{APIRefHash: apiRefHash}, time.Minute)
+	require.NoError(t, err)
+
+	uri := s.Router.GetRoute("GET", s.getItemLogsLinesHandler, map[string]string{
+		"type":   string(sdk.CDNTypeItemStepLog),
+		"apiRef": apiRefHash,
+	})
+	require.NotEmpty(t, uri)
+	req := assets.NewJWTAuthentifiedRequest(t, tokenRaw, "GET", uri, nil)
+	rec := httptest.NewRecorder()
+	s.Router.Mux.ServeHTTP(rec, req)
+	require.Equal(t, 200, rec.Code)
+
+	var lines []redis.Line
+	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &lines))
+	require.Len(t, lines, 1)
+	require.Equal(t, int64(2), lines[0].Number)
+	require.Equal(t, "[EMERGENCY] this is a message\n", lines[0].Value)
+}
diff --git a/engine/cdn/lru/redis_test.go b/engine/cdn/lru/redis_test.go
index 97cc32988..8b03af717 100644
--- a/engine/cdn/lru/redis_test.go
+++ b/engine/cdn/lru/redis_test.go
@@ -82,7 +82,7 @@ func TestRedisLRU(t *testing.T) {
 	require.Equal(t, int64(88), size)
 
 	// Get Item 1
-	reader := r.NewReader(item1.ID, 0, 1)
+	reader := r.NewReader(item1.ID, sdk.CDNReaderFormatText, 0, 1)
 	buf := new(strings.Builder)
 	_, err = io.Copy(buf, reader)
 	reader.Close()
@@ -110,5 +110,4 @@ func TestRedisLRU(t *testing.T) {
 	size, err = r.Size()
 	require.NoError(t, err)
 	require.Equal(t, int64(65), size)
-
 }
diff --git a/engine/cdn/storage/dao_test.go b/engine/cdn/storage/dao_test.go
index a2a09bff8..1a7518eec 100644
--- a/engine/cdn/storage/dao_test.go
+++ b/engine/cdn/storage/dao_test.go
@@ -25,7 +25,7 @@ func TestLoadOldItemUnitByItemStatusAndDuration(t *testing.T) {
 
 	cdntest.ClearItem(t, context.TODO(), m, db)
 
-	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, storage.Configuration{
+	cdnUnits, err := storage.Init(context.TODO(), m, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
 		Buffer: storage.BufferConfiguration{
 			Name: "redis_buffer",
 			Redis: storage.RedisBufferConfiguration{
@@ -33,7 +33,7 @@ func TestLoadOldItemUnitByItemStatusAndDuration(t *testing.T) {
 				Password: cfg["redisPassword"],
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 
 	// Clean old test
 	time.Sleep(1 * time.Second)
diff --git a/engine/cdn/storage/storageunit_test.go b/engine/cdn/storage/storageunit_test.go
index 0c9ae9fe1..59f681ceb 100644
--- a/engine/cdn/storage/storageunit_test.go
+++ b/engine/cdn/storage/storageunit_test.go
@@ -40,7 +40,7 @@ func TestRun(t *testing.T) {
 	tmpDir2, err := ioutil.TempDir("", t.Name()+"-cdn-2-*")
 	require.NoError(t, err)
 
-	cdnUnits, err := storage.Init(ctx, m, db.DbMap, storage.Configuration{
+	cdnUnits, err := storage.Init(ctx, m, db.DbMap, sdk.NewGoRoutines(), storage.Configuration{
 		Buffer: storage.BufferConfiguration{
 			Name: "redis_buffer",
 			Redis: storage.RedisBufferConfiguration{
@@ -77,7 +77,7 @@ func TestRun(t *testing.T) {
 				},
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err)
 	require.NotNil(t, cdnUnits)
 	require.NoError(t, cdnUnits.Start(ctx))
@@ -146,18 +146,18 @@ func TestRun(t *testing.T) {
 	localUnitDriver2 := cdnUnits.Storage(localUnit2.Name)
 	require.NotNil(t, localUnitDriver)
 
-	exists, err := localUnitDriver.ItemExists(*i)
+	exists, err := localUnitDriver.ItemExists(context.TODO(), m, db, *i)
 	require.NoError(t, err)
 	require.False(t, exists)
 
 	<-ctx.Done()
 
 	// Check that the first unit has been resync
-	exists, err = localUnitDriver.ItemExists(*i)
+	exists, err = localUnitDriver.ItemExists(context.TODO(), m, db, *i)
 	require.NoError(t, err)
 	require.True(t, exists)
 
-	exists, err = localUnitDriver2.ItemExists(*i)
+	exists, err = localUnitDriver2.ItemExists(context.TODO(), m, db, *i)
 	require.NoError(t, err)
 	require.True(t, exists)
 
diff --git a/engine/cdn/storage/swift/swift_test.go b/engine/cdn/storage/swift/swift_test.go
index 4d84b4eda..edd20d68d 100644
--- a/engine/cdn/storage/swift/swift_test.go
+++ b/engine/cdn/storage/swift/swift_test.go
@@ -17,7 +17,7 @@ import (
 func TestSwift(t *testing.T) {
 	log.SetLogger(t)
 	var driver = new(Swift)
-	err := driver.Init(context.TODO(), &storage.SwiftStorageConfiguration{
+	err := driver.Init(context.TODO(), sdk.NewGoRoutines(), &storage.SwiftStorageConfiguration{
 		Encryption: []convergent.ConvergentEncryptionConfig{
 			{
 				Cipher:      aesgcm.CipherName,
@@ -25,7 +25,7 @@ func TestSwift(t *testing.T) {
 				SecretValue: "secret_value",
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err, "unable to initialiaze webdav driver")
 
 	err = driver.client.ApplyEnvironment()
diff --git a/engine/cdn/storage/webdav/webdav_test.go b/engine/cdn/storage/webdav/webdav_test.go
index a02bedeff..eac5a8954 100644
--- a/engine/cdn/storage/webdav/webdav_test.go
+++ b/engine/cdn/storage/webdav/webdav_test.go
@@ -63,7 +63,7 @@ func TestWebdav(t *testing.T) {
 	t.Logf("webdav server running\n")
 
 	var driver = new(Webdav)
-	err = driver.Init(context.TODO(), &storage.WebdavStorageConfiguration{
+	err = driver.Init(context.TODO(), sdk.NewGoRoutines(), &storage.WebdavStorageConfiguration{
 		Address:  "http://localhost:8091",
 		Username: "username",
 		Password: "password",
@@ -75,7 +75,7 @@ func TestWebdav(t *testing.T) {
 				SecretValue: "secret_value",
 			},
 		},
-	}, sdk.NewGoRoutines())
+	})
 	require.NoError(t, err, "unable to initialiaze webdav driver")
 
 	itemUnit := sdk.CDNItemUnit{
diff --git a/sdk/util_test.go b/sdk/util_test.go
new file mode 100644
index 000000000..7ed7fc1c6
--- /dev/null
+++ b/sdk/util_test.go
@@ -0,0 +1,18 @@
+package sdk_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/ovh/cds/sdk"
+)
+
+func TestStringFirstN(t *testing.T) {
+	hash := "63cea1cffc5c9872b4741e5bb31ec85c0634a681"
+	hashShort := sdk.StringFirstN(hash, 7)
+	assert.Equal(t, "63cea1c", hashShort)
+	assert.Equal(t, "63cea1c", sdk.StringFirstN(hash, 7))
+	assert.Equal(t, "63ce", sdk.StringFirstN("63ce", 7))
+	assert.Equal(t, "", sdk.StringFirstN("", 7))
+}