Repository: ovh/cds
Pull request #5427 (2020-09-21 16:04:17)

feat(cdn): get item log lines in json
1. Description
1. Related issues
1. About tests
1. Mentions

@ovh/cds


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5427&metric=coverage&view=list) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo.png' alt='No Duplication information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5427&metric=duplicated_lines_density&view=list) No Duplication information


ovh-cds: 
CDS Report build-all-cds#14412.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Docs -   
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✔ 
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 
* Package and Analyze
  * Docker ✔ 
  * Sonar ✔ 
* Integration
  * Docker Compose Tests ✔ 
  * Standalone Tests ✘  
  * Standalone tests 1 -   
  * Standalone tests 2 -   
  * Standalone tests 3 -   

ovh-cds: 
CDS Report build-all-cds#14425.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Docs -   
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✘  
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5427&metric=coverage&view=list) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo.png' alt='No Duplication information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5427&metric=duplicated_lines_density&view=list) No Duplication information


ovh-cds: 
CDS Report build-all-cds#14432.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Docs -   
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✘  
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 

ovh-cds: 
CDS Report build-all-cds#14436.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Docs -   
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✘  
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5427&metric=coverage&view=list) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo.png' alt='No Duplication information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5427&metric=duplicated_lines_density&view=list) No Duplication information


ovh-cds: 
CDS Report build-all-cds#14441.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Docs -   
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✘  
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5427&metric=coverage&view=list) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo.png' alt='No Duplication information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5427&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5427&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5427&metric=coverage&view=list) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo.png' alt='No Duplication information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5427&metric=duplicated_lines_density&view=list) No Duplication information



Patch:
diff --git a/engine/api/admin.go b/engine/api/admin.go
index c419a6884..d56da7149 100644
--- a/engine/api/admin.go
+++ b/engine/api/admin.go
@@ -1,6 +1,7 @@
 package api
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"io/ioutil"
@@ -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/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..8a9685218 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 {
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/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..80ff04a3c 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
 	}
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/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/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/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..eeb282355 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.20200925082559-e4068ba25116
 	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..04fedaad8 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.20200925082559-e4068ba25116 h1:sLp92e6vkdTgP5C2bAkUzH0yyOSuNoTFjFOlBcoAN9Y=
+github.com/ovh/symmecrypt v0.4.4-0.20200925082559-e4068ba25116/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/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]
+}


Test patch:
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))
+}