kafka invalid timestamp error by pidstat

k8s 환경에서 프로세스등의 로그를 수집(collector pod) > json stdout(/var/log/pods/.../0.log) > filebeat > kafka cluster 로 흐름이 구축되어 있다.
그런데 자정(0시)에 가끔 collector CPU 90% 이 넘어가고 해소가 되지 않는다.

kafka(4.0.2) server log 를 보면 다음과 같이 invalid timestamp 로 거부가 됐다.
kafka 디폴트로 메시지 1시간까지 허용하고 넘어가면 거부된다.
org.apache.kafka.common.errors.InvalidTimestampException: One or more records have been rejected due to invalid timestamp

collector 는 pidstat 커맨드로 수집을 하는데 pidstat 는 Time=HH:MM:SS 으로 날짜가 출력되지 않는다.
# -h : 한 줄 가로 출력, 평균 행 없음 (파싱용)
# -u : CPU
# -r : 메모리
# -d : 디스크 I/O
# -l : Command 에 전체 커맨드라인
# -I : CPU 사용률을 전체 코어 수로 나눔 (SMP 정규화)
# -T : TASK + CHILD 통계 모두
# -p : 대상 PID (discover 가 찾은 프로세스들)
pidstat -hurdl -I -T ALL -p <PIDs> <interval> <count>

# Time        UID       PID    %usr %system  %guest   %wait    %CPU   CPU  minflt/s  majflt/s     VSZ     RSS   %MEM   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
05:04:08        0       630    0.00    0.00    0.00    0.00    0.00     9      0.00      0.00    2228    1024   0.01      0.00      0.00      0.00       0  sleep 300

이 Time을 collector 에서 파싱해서 @timestamp 필드값을 설정한다.

# 파싱 과정
# local_to_utc() python 로직
dt = datetime.strptime(local_timestamp, "%H:%M:%S")
return datetime.now().replace(hour=dt.hour, minute=dt.minute, second=dt.second).astimezone(tz=timezone.utc)

# timestamp 필드 설정
obj["@timestamp"] = local_to_utc(obj.pop("Time")).strftime("%Y-%m-%dT%H:%M:%SZ")

# 문제가 되는 상황 예시
pidstat 출력 23:59:59.6 (8/15) -> Time="23:59:59"
파서 처리 시점 00:00:00.2 (8/16) -> now().date = 8/16 -> timestamp = 2026-0816T23:59:59Z
실제 측정 시각 8/15 23:59:59 대비 +24h이 지난 상태의 timestamp 가 된다.
+24h 메시지는 kafka 에서 거부하는데, collector 는 계속 재시도를 해 cpu 리소스가 높게 유지 된 것이다.

# 해결 시도
# pidstat 와 1초 정도밖에 차이 안나서 이렇게 파싱시 now 를 timestamp 로 사용할 수 있겠지만
# pidstat 이 한 주기에 프로세스 n개를 측정하고 있어 원래는 n개의 라인이 전부 Time 이 같아야 한다. 매번 now 로 조금씩 달라지면 배치 동일성이 깨진다.
obj["@timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

# 해결 방법1
# +24(12시간 차이 정도로 하자)시간이 지난 경우 -1day 한다.
now = datetime.now()
ts = now.replace(hour=dt.hour, minute=dt.minute, second=dt.second, microsecond=0)
if ts - now > timedelta(hours=12):
    ts -= timedelta(days=1)
    logger.info(f"midnight rollover: Time={local_timestamp} -> {ts}")
return ts.astimezone(tz=timezone.utc)

# 해결 방법2
# pidstat 사용 안 하고 k8s 환경이면 이미 cAdvisor(ContainerAdvisor, kubelet 내장, 실행 중인 컨테이너들의 리소스 사용량과 성능 메트릭을 수집,처리,노출하는 도구)로 prometheus 에 메트릭이 있으니 이 값을 조회해서 사용한다.
# 메트릭들은 이미 같은 timestamp 로 저장되어 있다.
{"@timestamp": "2026-08-16T05:23:44Z", "%CPU": "0.88", "%usr": "35.59", "%system": "6.81",
   "RSS": "571528", "container": "ysoftman1-container", "pod": "ysoftman1-aaa"}
{"@timestamp": "2026-08-16T05:23:44Z", "%CPU": "0.88", "%usr": "35.59", "%system": "6.81",
   "RSS": "571528", "container": "ysoftman2-container", "pod": "ysoftman2-bbb"}
# 현재시각을 고정하고 이 값으로 prometheus 메트릭을 조회 하고 그대로 @timestamp 로 설정한다.
at = datetime.now(timezone.utc).timestamp()
ts = datetime.fromtimestamp(at, timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

# 어쩌다 발생해서 신경쓰였는데 원인 파악 돼서 개비스콘 짤로 남긴다.ㅎ

skill.sh(Vercel Agent Skills Ecosystem)

claude code 에서 스킬을 만들어 공유할때 보통 플러그인을 만드는데 skill.md 외 .claude-plugin/{marketplace.json,plugin.json} 을 만들어 버전,키워드등의 메타 데이터를 작성해야 한다.

그런데 최근 vercel 에서 오픈한 https://www.skills.sh 을 보면 그냥 skill.md 만 생성하고 별도의 .json 같은 파일을 필요로 하지 않는다.
마켓에 올리는 것이 아니니 별도의 심사가 없고 스킬이 있는 깃헙 저장소 자체의 버전이 되는 것이다.

claude code 플러그인은 설치하면 ~/.claude/plugin/ 에 추가되는 반면 skills 로 설치하면 사용자 로컬 ~/.claude/skills/ 에 일반 로컬 스킬로 추가된다

skills.sh 은 json 매니페스트를 복잡하게 구성할 필요 없이 .md 상단의 frontmatter 만으로 충분하다.

# 사용 예 (난 npx 보단 bunx 을 선호한다.)
# 특정 브랜치 지정해서 설치(디폴트는 main, HEAD)
bunx skills add my-org/my-skills#develop

# 특정 태그 지정해서 설치
bunx skills add my-org/my-skills#v1.2.0

# 특정 커밋 해시를 지정해서 설치
bunx skills add my-org/my-skills#a1b2c3d

# 다음과 같이 특정 저장소 경로를 명시해서 설치할 수도 있다.
bunx skills add https://github.com/ysoftman/myenv/tree/main/.claude/skills

# https://github.com/Leonxlnx/taste-skill 설치 예시
bunx skills add https://github.com/Leonxlnx/taste-skill --skill "design-taste-frontend"

googleworkspace-cli

gws(https://github.com/googleworkspace/cli) 커맨드로 캘린더 조회
gws 설치
brew install googleworkspace-cli

인증/로그인
1. 구글 콘솔 로그인(예: https://console.cloud.google.com/auth/clients?project=watchdust)
2. 테스트 사용자 섹션 > Add users > ysoftman@gmail.com 추가
https://console.cloud.google.com/auth/audience?project=watchdust
3. 클라이언트 만들기 > 애플리케이션 유형 데스크톱 앱(터미널 커맨드라 앱을 선택해야 한다.) > 이름 gws 생성
4. JSON 다운로드 후 교체
mv ~/Downloads/client_secret_*.json ~/.config/gws/client_secret.json
chmod 600 ~/.config/gws/client_secret.json
5. gws auth login
터미널에 표시되는 url 이 길러 클릭하면 잘릴 수 있어 전체 복사해서 브라우저에서 수동으로 열어야 했다.

테스트 모드에서 발급된 refresh token 은 7일 후 만료돼 다시 로그인이 필요하다.

# gws 사용 예시
# 안 읽은 메일
gws gmail +triage
gws gmail +triage --max 5 --query 'from:noreply@crates.io'
gws gmail +triage --format json | jq -r '.[].subject'

# 읽은 메일까지 보기
gws gmail +triage --query 'in:inbox' --max 10

# 본문 읽기
gws gmail +read --id MESSAGE_ID
gws gmail +read --id MESSAGE_ID --headers

# 메일 보내기
gws gmail +send --to alice@example.com --subject '테스트' --body '내용' --dry-run
gws gmail +send --to alice@example.com --subject '보고서' --body '첨부 확인' -a report.pdf
gws gmail +send --to alice@example.com --subject '초안' --body '나중에' --draft

# 메일 답장
gws gmail +reply --id MESSAGE_ID --body '확인했습니다'

# 메일 전달
gws gmail +forward --id MESSAGE_ID --to bob@example.com

# 앞으로 일정
gws calendar +agenda
gws calendar +agenda --today
gws calendar +agenda --week --format table
gws calendar +agenda --days 3 --calendar 'Work'

# 일정 추가
gws calendar +insert --summary '스탠드업' \
  --start '2026-08-11T09:00:00+09:00' --end '2026-08-11T09:30:00+09:00'
gws calendar +insert --summary '리뷰' \
  --start '2026-08-11T14:00:00+09:00' --end '2026-08-11T15:00:00+09:00' \
  --attendee alice@example.com --meet

# 일정 삭제
gws calendar events delete --params '{"calendarId":"primary","eventId":"EVENT_ID"}'

# 드라이브 파일 조회
gws drive files list --params '{"pageSize":10,"fields":"files(id,name,mimeType,modifiedTime)"}' --format table

# 보고서 단어가 포함된 파일 조회
gws drive files list --params '{"q":"name contains \"보고서\" and trashed=false"}' --format table

# pdf 형식 파일 조회
gws drive files list --params '{"q":"mimeType=\"application/pdf\""}' --format table

# 파일 업로드
gws drive +upload ./report.pdf
gws drive +upload ./data.csv --name '매출.csv' --parent FOLDER_ID

# 파일 다운로드
gws drive files export --params '{"fileId":"DOC_ID","mimeType":"application/pdf"}' -o doc.pdf

Prev